Runtime do Serve
Visão Geral
packages/cli/src/serve/ é a camada de inicialização para qwen serve. Ela traduz as flags da CLI em ServeOptions, valida a configuração de inicialização, constrói o aplicativo Express, conecta middlewares, registra rotas, expõe provedores de preflight/status do daemon, mantém o anel de auditoria de permissões e gerencia a sequência de desligamento gracioso em duas fases. O trabalho voltado para HTTP reside nesta camada; o trabalho voltado para ACP reside uma camada abaixo em @qwen-code/acp-bridge (veja 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 MCP, CORS, timeouts de prompt/SSE/sessão ociosa, limite de taxa e opções relacionadas. - Canonicalizar o workspace vinculado exatamente uma vez. A mesma forma canônica é compartilhada por
/capabilities, o fallbackPOST /sessione a bridge. - Rejeitar configurações de inicialização inseguras ou inválidas: bind sem token para endereço não loopback,
--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 limite de taxa. - Construir a fábrica
WorkspaceFileSystem, o publicador de auditoria de permissões, oDaemonStatusProvidere aacp-bridge. - Construir o aplicativo Express, conectar middlewares (
denyBrowserOriginCors/allowOriginCors->hostAllowlist-> log de acesso ->bearerAuth-> limite de taxa -> parser JSON -> telemetria ->mutationGatepor rota) e montar rotas de sessão, CRUD de workspace, arquivo, autenticação por device flow, voto de permissão e ACP HTTP. - Vincular a porta de escuta e registrar manipuladores de sinal.
- Executar desligamento em duas fases ao receber SIGINT/SIGTERM; forçar saída em um segundo sinal.
Arquitetura
Entrada: runQwenServe(opts, deps) em packages/cli/src/serve/run-qwen-serve.ts. Retorna um RunHandle ({ url, port, close, ... }).
Fábrica do app: createServeApp(opts, getPort, deps) em packages/cli/src/serve/server.ts. Constrói a Application do Express. Incorporadores diretos e testes chamam-na sem o invólucro de inicialização.
Registro de capacidades: SERVE_CAPABILITY_REGISTRY em packages/cli/src/serve/capabilities.ts. Cada tag possui 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 a respectiva opção está desligada. Veja 11-capabilities-versioning.md.
Middlewares (packages/cli/src/serve/auth.ts e server.ts):
| Middleware, na ordem de registro | Propósito | Notas |
|---|---|---|
denyBrowserOriginCors / allowOriginCors | Negar todos os cabeçalhos Origin por padrão; alternar para uma lista de permissões quando --allow-origin <pattern> está configurado. | Veja 12-auth-security.md. |
hostAllowlist(bind, getPort) | Em loopback, validar se Host pertence a localhost, 127.0.0.1, [::1] ou host.docker.internal mais a porta real. | Defesa contra DNS rebinding. A comparação é insensível a maiúsculas/minúsculas e cacheada por porta. |
| Middleware de log de acesso | Registra método, caminho, status, duraçãoMs, sessionId e clientId em DaemonLogger quando uma requisição termina. | Registrado antes do bearerAuth, para que negações 401 também sejam registradas. Ignora /health e heartbeat. |
bearerAuth(token) | SHA-256 mais comparação constante de bearer com timingSafeEqual. | Passagem livre quando nenhum token está configurado (padrão de desenvolvimento em loopback). Esquema Bearer é insensível a maiúsculas/minúsculas. |
| Middleware de limite de taxa | Token bucket opcional por camada para rotas de prompt, mutação e leitura. | Registrado após bearerAuth e antes da análise JSON; retorna 429 antes da análise quando um bucket está esgotado. |
express.json({ limit: '10mb' }) | Análise do corpo JSON. | Erros de análise retornam 400. |
daemonTelemetryMiddleware | Envolve cada requisição HTTP em um span do OpenTelemetry por meio de withDaemonRequestSpan. | Atributos incluem rota, sessionId, clientId e código de status. |
createMutationGate (por rota) | Portão de aceitação opcional por rota para rotas de mutação que exigem token mesmo em loopback. | Retorna 401 { code: 'token_required' }. Não é app.use global; as rotas chamam mutate({ strict: true }) conforme necessário. |
| Subsistemas: |
| Caminho | Função |
|---|---|
serve/fs/ | Fábrica WorkspaceFileSystem mais policy.ts (verificações de tamanho/confiança/binários), paths.ts (canonicalizar, resolveWithin, rejeição de symlink), audit.ts e valores FsError tipados. |
serve/routes/workspace-file-read.ts, workspace-file-write.ts | Manipuladores 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 subagent). |
serve/daemon-status-provider.ts | Snapshot de env mais células de preflight do host do daemon: 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 estruturados em arquivo 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 HTTP Streamable ACP (RFD #721), montado em /acp. Sete arquivos implementam JSON-RPC POST, SSE GET, DELETE teardown e uso compartilhado de bridge em paralelo com a superfície REST. |
serve/demo.ts | HTML inline autocontido para GET /demo: console de depuração do navegador com UI de chat, log de eventos e inspetor de workspace. Em loopback sem --require-auth, é registrado antes de bearerAuth; em não-loopback ou com --require-auth, é registrado depois de 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. |
| Re‑export shims para compatibilidade com caminhos de importação pré‑F1: |
serve/event-bus.ts->@qwen-code/acp-bridge/eventBusserve/status.ts->@qwen-code/acp-bridge/statusserve/httpAcpBridge.ts->@qwen-code/acp-bridge
Fluxo
Sequência de inicialização
- Resolver e aparar o token a partir de
opts.tokenouQWEN_SERVER_TOKEN; isso evita que uma quebra de linha final decat token.txtsilenciosamente quebre a comparação do bearer. - Proteção contra erro de digitação em hostname:
--hostname localhost:4170gera erro e sugere--port. - Verificação prévia de autenticação: sem loopback sem token recusa;
--require-authsem token recusa. - Validação do workspace: caminho absoluto, existe, é diretório.
EACCES/EPERMsão encapsuladas para apontar para a flag. - Canonicalização do workspace:
canonicalizeWorkspace(rawWorkspace)executarealpathSync.nativeuma vez e alimenta/capabilities, o fallbackPOST /sessione a bridge. - Validação do orçamento MCP: inteiro positivo;
enforceexige um orçamento. - Inferência da alternância do pool MCP: a variável de ambiente pai
QWEN_SERVE_NO_MCP_POOL=1fazmcpPoolActive=false, então as capabilities omitem honestamentemcp_workspace_poolemcp_pool_restart. - Validação de CORS / timeout / rate‑limit:
--allow-origin '*'exige token; os valores de prompt, writer, channel idle, session idle, reaper e janela de rate‑limit falham rapidamente quando inválidos. childEnvOverridespor handle: passarQWEN_SERVE_MCP_CLIENT_BUDGETeQWEN_SERVE_MCP_BUDGET_MODEpara o filho ACP através deBridgeOptions.childEnvOverridesem vez de mutarprocess.env.- Carregar
settings.jsonuma vez: lercontext.fileName,policy.permissionStrategyepolicy.consensusQuorum. Arquivos corrompidos recaem nos padrões.validatePolicyConfig()verificapolicy.*contraSERVE_CAPABILITY_REGISTRY.permission_mediation.modes; estratégias desconhecidas ouconsensusQuorumnão positivo lançamInvalidPolicyConfigError. Um quorum definido sob uma estratégia que não éconsensusregistra um aviso em stderr. - Alocar
PermissionAuditRing(512 entradas). - Construir
fsFactory:runQwenServepadrão étrusted: true; chamadores diretos decreateServeApppadrão étrusted: falsee avisam uma vez. createHttpAcpBridge, veja03-acp-bridge.md.createServeAppmonta o Express.server.listen(port, hostname), então resolver ogetPort()real para a lista de permissões de host.- Registrar handlers SIGINT / SIGTERM para desligamento gracioso.
Desligamento gracioso
- Fase 1 – desmontagem da bridge no primeiro sinal:
- Descartar o registro de fluxo do dispositivo e cancelar fluxos pendentes.
bridge.shutdown()marca cada canal comoisDying = true, envia fechamento gracioso para cada stdin do filho ACP, aguardaKILL_HARD_DEADLINE_MS(10s) por canal, então chamachannel.kill()se necessário.
- Fase 2 – desmontagem HTTP:
server.close()para de aceitar novas conexões e deixa as requisições em andamento terminarem.SHUTDOWN_FORCE_CLOSE_MS(5s) disparaserver.closeAllConnections().- Um segundo prazo de 2s escala novamente se necessário.
- Segundo sinal durante a saída:
bridge.killAllSync()+process.exit(1)para evitar filhos órfãos bloqueando a saída do daemon.
Estado e ciclo de vida
RunHandle expõe:
url: URL de escuta resolvida, após resolução de porta efêmera.port: porta real, incluindo resolução de0.close({ timeoutMs? }): desligamento programático para embutidores e testes.
Chamar createServeApp diretamente retorna apenas uma Application; o embutidor é dono do listen e do desligamento.
Dependências
Upstream usado por serve/ | Downstream que usa serve/ |
|---|---|
@qwen-code/acp-bridge: bridge, barramento de eventos, tipos de status | O handler do subcomando serve da CLI qwen |
packages/core: loadSettings, getCurrentGeminiMdFilename, Config, WorkspaceContext | Embutidores diretos, testes |
SDK ACP (@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 aparar. |
| Env | QWEN_SERVE_NO_MCP_POOL=1 | Força mcpPoolActive=false. |
| Env filho ACP | QWEN_SERVE_MCP_CLIENT_BUDGET / QWEN_SERVE_MCP_BUDGET_MODE | Gerado a partir de --mcp-client-budget / --mcp-budget-mode e encaminhado via childEnvOverrides. |
| Env | QWEN_SERVE_PROMPT_DEADLINE_MS / QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | Timeouts padrão de prompt / idle do SSE. |
| Env | QWEN_SERVE_RATE_LIMIT* | Interruptor de rate‑limit, limites de prompt / mutation / leitura e janela padrão. |
| Env | QWEN_SERVE_DEBUG=1 | Logs verbosos em stderr. Veja 19-observability.md. |
| Flags | --hostname, --port | Vinculação de escuta. |
| Flags | --token, --require-auth, --enable-session-shell | Token Bearer, endurecimento de autenticação em loopback e chave explícita 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} | Encaminhados para o filho ACP. |
| Flags | --allow-origin, --allow-private-auth-base-url | Lista de permissões CORS do navegador e chave de instalação do provedor de autenticação localhost/privado. |
| Flags | --prompt-deadline-ms, --writer-idle-timeout-ms, --channel-idle-timeout-ms | Controle de ciclo de vida idle do prompt, escritor SSE e filho ACP. |
| Flags | --session-reap-interval-ms, --session-idle-timeout-ms | Controle de coleta de sessões desconectadas. |
| Flags | --rate-limit* | Rate‑limit HTTP por nível. |
settings.json | policy.permissionStrategy, policy.consensusQuorum | Política do MultiClientPermissionMediator e quorum. |
settings.json | context.fileName | Substituição de getCurrentGeminiMdFilename para a bridge. |
Veja 17-configuration.md para a referência mesclada. |
Ressalvas e limitações conhecidas
- O
createServeAppdireto semdeps.fsFactoryoudeps.bridgepadrão paratrusted: false; o ACP do lado do agentewriteTextFilerejeita comountrusted_workspace. O aviso é impresso apenas uma vez. denyBrowserOriginCorsrejeita todas as requisições que trazemOrigin; a página de demonstração funciona porque outro middleware remove valores de mesma origem correspondentes primeiro.- Ordenação do body-parser: rotas que usam
mutate({ strict: true })retornam 401 apenas apósexpress.json(). O pior caso é--max-connections × express.json({limit: '10mb'}), até cerca de 2,5 GB de memória transitória em um listener de loopback saturado; essa compensação é intencional. - Múltiplos daemons em um processo devem usar
childEnvOverridespor handle; mutarprocess.envcausa condições de corrida porquedefaultSpawnChannelFactorycaptura o env no momento da criação.
Referências
packages/cli/src/serve/run-qwen-serve.ts(inicialização, validação de inicialização, desligamento gracioso)packages/cli/src/serve/server.ts(createServeApp(), montagem de middleware e rotas)packages/cli/src/serve/auth.ts(CORS, lista de permissão de Host, auth bearer, gate de mutação)packages/cli/src/serve/rate-limit.ts(limite de taxa HTTP por camada)packages/cli/src/serve/capabilities.ts(registro de capacidades 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