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 vinculado exatamente uma vez. A mesma forma canônica é compartilhada por
/capabilities, o fallback dePOST /sessione a bridge. - Rejeitar configurações de inicialização inseguras ou inválidas: bind fora do loopback sem token,
--require-authsem token,--allow-origin '*'sem token,mcpBudgetMode='enforce'sem ummcpClientBudgetpositivo, um--workspaceinexistente 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, oDaemonStatusProvidere aacp-bridge. - Construir o app Express, conectar os middlewares (
denyBrowserOriginCors/allowOriginCors->hostAllowlist-> access log ->bearerAuth-> rate limit -> JSON parser -> telemetry ->mutationGatepor 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. - 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. Dez tags condicionais (require_auth, mcp_workspace_pool, mcp_pool_restart, allow_origin, prompt_absolute_deadline, writer_idle_timeout, workspace_settings, session_shell_command, rate_limit, workspace_reload) são omitidas quando seu respectivo toggle está desativado. Consulte 11-capabilities-versioning.md.
Middleware (packages/cli/src/serve/auth.ts e server.ts):
| Middleware, em ordem de registro | Propósito | Notas |
|---|---|---|
denyBrowserOriginCors / allowOriginCors | Negar todos os headers Origin por padrão; alternar para uma allowlist quando --allow-origin <pattern> estiver configurado. | 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. |
| Access-log middleware | Registra 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 middleware | Token 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. |
daemonTelemetryMiddleware | Envolve cada requisição HTTP em um span do OpenTelemetry através do withDaemonRequestSpan. | Os atributos incluem rota, sessionId, clientId e código de status. |
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:
| Caminho | Funçã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.ts | Handlers HTTP para GET /file, GET /file/bytes, POST /file/write e POST /file/edit. |
serve/workspace-memory.ts | GET/POST /workspace/memory (CRUD do QWEN.md). |
serve/workspace-agents.ts | GET/POST/DELETE /workspace/agents (CRUD de subagentes). |
serve/daemon-status-provider.ts | Snapshot 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.ts | PermissionAuditRing (FIFO de 512 entradas) e createPermissionAuditPublisher. |
serve/auth/device-flow.ts, qwen-device-flow-provider.ts | Rotas OAuth de device-flow. Consulte 12-auth-security.md. |
serve/daemon-logger.ts | Logs de arquivo estruturados do DaemonLogger. Consulte 19-observability.md. |
serve/debug-mode.ts | Predicado 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/demo.ts | HTML inline autossuficiente para GET /demo: console de debug do navegador com UI de chat, log de eventos e inspetor de workspace. No loopback sem --require-auth, é registrado antes do bearerAuth; fora do loopback ou com --require-auth, é registrado depois do bearerAuth. Servido com CSP default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none' mais X-Frame-Options: DENY. |
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.tspermanece como a facade de compatibilidade local da CLI para a superfície mais ampla da bridge.
Fluxo
Sequência de inicialização
- Resolver e remover espaços do token em
opts.tokenouQWEN_SERVER_TOKEN; isso evita que uma nova linha final decat token.txtquebre silenciosamente a comparação do bearer. - Proteção contra erro de digitação no hostname:
--hostname localhost:4170gera um erro e sugere--port. - Preflight de autenticação: fora do loopback sem token é recusado;
--require-authsem token é recusado. - Validação do workspace: caminho absoluto, existe, é diretório.
EACCES/EPERMsão encapsulados para apontar para a flag. - Canonicalizar workspace:
canonicalizeWorkspace(rawWorkspace)executarealpathSync.nativeuma vez e alimenta/capabilities, o fallback dePOST /sessione a bridge. - Validação do orçamento do MCP: inteiro positivo;
enforceexige um orçamento. - Inferência do toggle do pool MCP: a env pai
QWEN_SERVE_NO_MCP_POOL=1tornamcpPoolActive=false, para que as capacidades omitam honestamentemcp_workspace_poolemcp_pool_restart. - 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. childEnvOverridespor handle: passaQWEN_SERVE_MCP_CLIENT_BUDGETeQWEN_SERVE_MCP_BUDGET_MODEpara o filho ACP através deBridgeOptions.childEnvOverridesem vez de mutarprocess.env.- Carregar
settings.jsonuma vez: lêcontext.fileName,policy.permissionStrategyepolicy.consensusQuorum. Arquivos corrompidos recorrem aos padrões.validatePolicyConfig()verificapolicy.*contraSERVE_CAPABILITY_REGISTRY.permission_mediation.modes; estratégias desconhecidas ouconsensusQuorumnão positivo lançamInvalidPolicyConfigError. Um quórum definido sob uma estratégia nãoconsensusregistra um aviso no stderr. - Alocar
PermissionAuditRing(512 entradas). - Construir
fsFactory:runQwenServeusa como padrãotrusted: true; chamadores diretos decreateServeAppusam como padrãotrusted: falsee avisam uma vez. createHttpAcpBridge, consulte03-acp-bridge.md.createServeAppmonta o Express.server.listen(port, hostname), depois resolve ogetPort()real para a allowlist de hosts.- Registrar manipuladores SIGINT / SIGTERM para desligamento gracioso.
Desligamento gracioso
- Fase 1 - teardown da bridge no primeiro sinal:
- Descartar o registro de device-flow e cancelar fluxos pendentes.
bridge.shutdown()marca cada canal comoisDying = true, envia um fechamento gracioso para o stdin de cada filho ACP, aguardaKILL_HARD_DEADLINE_MS(10s) por canal e então chamachannel.kill()se necessário.
- Fase 2 - teardown HTTP:
server.close()para de aceitar novas conexões e deixa as requisições em andamento terminarem.SHUTDOWN_FORCE_CLOSE_MS(5s) acionaserver.closeAllConnections().- Um segundo prazo de 2s escala novamente se necessário.
- 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 de0.close({ timeoutMs? }): desligamento programático para embedders e testes.
Chamar createServeApp diretamente retorna apenas um Application; o embedder é responsável pelo listen e pelo desligamento.
Dependências
Upstream usado por serve/ | Downstream usando serve/ |
|---|---|
@qwen-code/acp-bridge: bridge, event bus, tipos de status | O handler do subcomando serve da CLI qwen |
packages/core: loadSettings, getCurrentGeminiMdFilename, Config, WorkspaceContext | Embedders diretos, testes |
ACP SDK (@agentclientprotocol/sdk): PROTOCOL_VERSION, ClientSideConnection através da bridge | |
Express + body-parser, node:crypto, node:fs, node:path |
Configuração
| Origem | Chave | Efeito |
|---|---|---|
| Env | QWEN_SERVER_TOKEN | Token bearer após remoção de espaços. |
| Env | QWEN_SERVE_NO_MCP_POOL=1 | Força mcpPoolActive=false. |
| Env do filho ACP | QWEN_SERVE_MCP_CLIENT_BUDGET / QWEN_SERVE_MCP_BUDGET_MODE | Gerado a partir de --mcp-client-budget / --mcp-budget-mode e encaminhado através de childEnvOverrides. |
| Env | QWEN_SERVE_PROMPT_DEADLINE_MS / QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | Timeouts padrão de inatividade de prompt / SSE. |
| Env | QWEN_SERVE_RATE_LIMIT* | Switch de rate limit, limites de prompt / mutação / leitura e padrão de janela. |
| Env | QWEN_SERVE_DEBUG=1 | Logs detalhados no stderr. Consulte 19-observability.md. |
| Flags | --hostname, --port | Vinculação de escuta. |
| Flags | --token, --require-auth, --enable-session-shell | Token bearer, reforço de autenticação no loopback e switch explícito de execução de shell. |
| Flag | --workspace | Substitui process.cwd(). |
| Flags | --max-sessions, --max-pending-prompts-per-session, --max-connections, --event-ring-size | Limites 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-url | Allowlist de CORS do navegador e switch de instalação do provedor de autenticação localhost/privado. |
| Flags | --prompt-deadline-ms, --writer-idle-timeout-ms, --channel-idle-timeout-ms | Controle do ciclo de vida de inatividade de prompt, writer SSE e filho ACP. |
| Flags | --session-reap-interval-ms, --session-idle-timeout-ms | Controle de reaping de sessões desconectadas. |
| Flags | --rate-limit* | Limite de taxa HTTP por tier. |
settings.json | policy.permissionStrategy, policy.consensusQuorum | Política e quórum do MultiClientPermissionMediator. |
settings.json | context.fileName | Substituição de getCurrentGeminiMdFilename para a bridge. |
Consulte 17-configuration.md para a referência consolidada. |
Ressalvas e limites conhecidos
- O uso direto de
createServeAppsemdeps.fsFactoryoudeps.bridgetem como padrãotrusted: false; owriteTextFiledo ACP no lado do agente rejeita comountrusted_workspace. O aviso é impresso apenas uma vez. - O
denyBrowserOriginCorsrejeita todas as requisições que carregamOrigin; a página de demonstração funciona porque outro middleware remove primeiro os valores correspondentes de mesma origem. - Ordem do body-parser: rotas que usam
mutate({ strict: true })retornam 401 apenas após oexpress.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
childEnvOverridespor handle; a mutação deprocess.envcausa race conditions porque odefaultSpawnChannelFactoryfaz 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.tspackages/cli/src/serve/permission-audit.ts- Issues: #3803 , #4175