Skip to Content
DesignDesign de Implementação do DaemonWorkspaceService (Plano C)

Design de Implementação do DaemonWorkspaceService (Plano C)

Relacionado: issue #4542, PR #4472, #3803, #4175 Branch: daemon_mode_b_main Data: 2026-05-27 Natureza: Documento de design de implementação (focado na execução), não é um RFC


Escopo de Implementação (atualizado em 2026-05-31, PR #4563)

Este documento descreve a arquitetura final. O PR #4563 implementa apenas uma parte dele, o restante fica para PRs subsequentes. Ao ler, considere a tabela abaixo e não assuma que tudo já foi implementado:

CapacidadeStatus neste PR (#4563)
Renomeação de HttpAcpBridge para AcpSessionBridge✅ Implementado
Bridge expondo delegações genéricas queryWorkspaceStatus / invokeWorkspaceCommand✅ Implementado
status / init / tool-toggle / mcp-restart no nível do workspace da facade✅ Implementado e conectado (server.ts + acpHttp dispatch via facade)
Quatro sub-services: File / Auth / Agents / Memoryadiado — não está neste PR. Será implementado em PRs futuros, junto com suas respectivas conexões de rotas, injeção de deviceFlowRegistry/subagentManager e testes e2e
Rotas REST como /workspace/memory, /workspace/agents chamando a facadeadiado — atualmente ainda servidas diretamente pelos antigos workspaceMemory.ts / workspaceAgents.ts
Dispatch /acp northbound qwen/workspace/* (§6)adiado
initWorkspace usando fsFactory / WorkspaceFileSystem (trust gate + audit)adiado — atualmente reutiliza a implementação raw node:fs da bridge antiga (incluindo proteção §SV TOCTOU/symlink), sem regressão; migração do fsFactory/audit fica para depois

Portanto, a §3.4 (interfaces de sub-service), §6 (/acp northbound), e2e.test.ts da §7.1 e a descrição do formato do PR da §10 pertencem ao escopo final/futuro e não foram implementadas neste PR.


1. Arquitetura e Limites

1.1 Camadas da Arquitetura Final

CLIENTS webui SDK/channels(via REST) Zed/Goose(/acp) future │ │ │ ═════╪═════════════╪═══════════════════════╪═════════════ L1 transport (fino) REST+SSE REST+SSE /acp (jsonrpc/sse) server.ts acpHttp/ └─────────────┴───────────────────────┘ │ negócios/trust/audit sempre descem para L2 ═════════════════════════╪═══════════════════════════════ L2 camada de aplicação ┌──────────────────────────┐ ┌─────────────────────────────────┐ │ AcpSessionBridge │ │ DaemonWorkspaceService (facade) │ │ (← renomeado de HttpAcpBridge)│ │ ┌──────────────────────────┐ │ │ • ciclo de vida channel/session │ │ │ FileService │ │ │ • prompt / cancel / close │ │ │ AuthService │ │ │ • EventBus / arbitragem de permissões │ │ │ AgentsService │ │ │ • introspecção de estado dependente do child │ │ │ MemoryService │ │ │ (mcp/skills/preflight) │ │ └──────────────────────────┘ │ └──────────┬───────────────┘ │ WorkspaceRequestContext unificado │ │ └──────────┬──────────────────────┘ │ L3 → child │ ▼ │ (puramente local, não toca no child) ══════════════════════════════════════════════════════════ L3 ACP-client ══════════════════════════════════════════════════════════ L4 agent

1.2 Função de Decisão de Divisão

Única regra: o escopo da operação é session ou workspace?

  • session-scoped (opera em um sessionId específico: prompt/cancel/close/model/approval/metadata/heartbeat) → permanece em AcpSessionBridge
  • workspace-scoped (opera no workspace como um todo: file/auth/agents/memory/mcp-status/skills/env/preflight/tool-toggle/init) → vai para DaemonWorkspaceService

Alguns métodos do workspace precisam consultar o child (status getters, restartMcpServer), o que é delegado ao channel da bridge por meio de callbacks injetados; o service em si não mantém a connection.

1.3 Dependências Transversais: Injeção de Callback (sem infra compartilhada)

Atualmente, publishWorkspaceEvent e knownClientIds são mantidos pela bridge (fan-out do bus por session / derivados da session). O service os utiliza por meio de injeção de callback unidirecional, sem introduzir uma camada de infraestrutura compartilhada.

Motivo:

  1. O EventBus é um bus por session (bridge.ts:1457), e o bus no nível do workspace já está planejado nos comentários do código para o PR 24 (bridge.ts:2611).
  2. knownClientIds também é derivado do estado de session-attach, e os comentários indicam claramente “PR 24 will replace it” (bridge.ts:2658).
  3. Esses dois itens são trabalhos independentes já iniciados; forçá-los neste PR equivaleria a adicionar uma refatoração extra.
  4. A injeção de callback é uma dependência unidirecional para o service (mantém apenas a referência da função, não sabe que vem da bridge); após a implementação do PR 24, basta trocar a fonte de injeção, a interface do service permanece inalterada.

Regras rígidas:

  1. Não deve haver referências de tipo AcpSessionBridge em DaemonWorkspaceServiceDeps — use apenas assinaturas de função.
  2. A bridge expõe externamente dois novos métodos, queryWorkspaceStatus e invokeWorkspaceCommand, para que o service os chame via callback. Internamente, continua usando a lógica existente de requestWorkspaceStatus / liveChannelInfo + timeout, sem criar novas abstrações.

2. Sequência de Construção e Injeção de Dependência

// Sequência de construção em runQwenServe.ts // 1. fsFactory construído primeiro (compartilhado por ambos) const fsFactory = resolveBridgeFsFactory({ ... }); // 2. bridge construída primeiro (é a owner de session/channel/EventBus) const bridge = createAcpSessionBridge({ eventRingSize, boundWorkspace, fileSystem: createBridgeFileSystemAdapter(fsFactory), // ... outros parâmetros existentes inalterados }); // 3. service construído depois, recebe o conjunto de callbacks da bridge const workspace = createDaemonWorkspaceService({ fsFactory, deviceFlowRegistry, subagentManager, boundWorkspace, contextFilename, // callbacks transversais — o service não sabe que vêm da bridge publishWorkspaceEvent: (event) => bridge.publishWorkspaceEvent(event), knownClientIds: () => bridge.knownClientIds(), // callbacks delegados ao child — métodos ext workspace-scoped chegam ao agent via channel da bridge queryWorkspaceStatus: (method, idle) => bridge.queryWorkspaceStatus(method, idle), invokeWorkspaceCommand: (method, params, opts) => bridge.invokeWorkspaceCommand(method, params, opts), }); // 4. Ambos são passados para as rotas do server + handler /acp createServeApp({ bridge, workspace, ... });

A ordem de construção bridge → service é uma dependência rígida (o service precisa dos métodos na instância da bridge como fonte de callback).


3. Estrutura Interna do DaemonWorkspaceService

3.1 Layout de Diretórios

packages/cli/src/serve/workspace-service/ ├── types.ts ← WorkspaceRequestContext + interfaces de sub-service ├── index.ts ← factory da facade (createDaemonWorkspaceService) ├── fileService.ts ← envolve fsFactory ├── authService.ts ← envolve DeviceFlowRegistry ├── agentsService.ts ← envolve SubagentManager ├── memoryService.ts ← envolve operações de arquivo de memória └── __tests__/ ├── fileService.test.ts ├── authService.test.ts ├── agentsService.test.ts ├── memoryService.test.ts └── e2e.test.ts

3.2 Interface da Facade

export interface DaemonWorkspaceService { file: FileService; auth: AuthService; agents: AgentsService; memory: MemoryService; // Puramente local initWorkspace( opts: InitWorkspaceOpts, ctx: WorkspaceRequestContext, ): Promise<void>; setToolEnabled( toolName: string, enabled: boolean, ctx: WorkspaceRequestContext, ): Promise<ToolToggleResult>; // Delegado ao child via callback getMcpStatus(): Promise<ServeWorkspaceMcpStatus>; getSkillsStatus(): Promise<ServeWorkspaceSkillsStatus>; getProvidersStatus(): Promise<ServeWorkspaceProvidersStatus>; getEnvStatus(): Promise<ServeWorkspaceEnvStatus>; getPreflightStatus(): Promise<ServeWorkspacePreflightStatus>; restartMcpServer( serverName: string, ctx: WorkspaceRequestContext, opts?: RestartOpts, ): Promise<RestartResult>; }

listWorkspaceSessions / recordHeartbeat / getHeartbeatState / publishWorkspaceEvent / knownClientIds permanecem na bridge — eles acessam o estado por session interno da bridge (mapa byId / session bus), sendo infraestrutura derivada da session. O service os consome via callback, não os possui diretamente.

3.3 Assinatura da Facade Factory

export interface DaemonWorkspaceServiceDeps { fsFactory: WorkspaceFileSystemFactory; deviceFlowRegistry: DeviceFlowRegistry; subagentManager: SubagentManager; boundWorkspace: string; contextFilename: string; persistDisabledTools: ( workspace: string, tool: string, enabled: boolean, ) => Promise<void>; // callbacks transversais (infraestrutura derivada da session) publishWorkspaceEvent: (event: WorkspaceEvent) => void; knownClientIds: () => Set<string>; // callbacks delegados ao child (métodos ext workspace-scoped chegam ao agent via channel da bridge) queryWorkspaceStatus: <T>(method: string, idle: () => T) => Promise<T>; invokeWorkspaceCommand: <T>( method: string, params?: Record<string, unknown>, opts?: { timeoutMs?: number }, ) => Promise<T>; } export function createDaemonWorkspaceService( deps: DaemonWorkspaceServiceDeps, ): DaemonWorkspaceService;

3.4 Interfaces de Cada Sub-service

Sub-serviceMétodosDeps necessáriosOrigem existente
FileServiceread, readBytes, write, edit, glob, list, statfsFactory, boundWorkspaceserve/routes/workspaceFileRead.ts, workspaceFileWrite.ts, serve/fs/
AuthServicestartFlow, getFlowStatus(flowId), cancelFlow(flowId), getAuthStatusdeviceFlowRegistryserve/auth/deviceFlow.ts, server.ts:794-966
AgentsServicelist, get(agentType), create, update, deletesubagentManager, publishWorkspaceEvent, knownClientIdsserve/workspaceAgents.ts
MemoryServicelist, read, write, deletefsFactory or direct fs, publishWorkspaceEvent, knownClientIdsserve/workspaceMemory.ts

O primeiro parâmetro de cada método é ctx: WorkspaceRequestContext, e o trust gate é executado de forma unificada na entrada do método.


4. WorkspaceRequestContext

export interface WorkspaceRequestContext { originatorClientId?: string; // header X-Qwen-Client-Id (pode estar ausente em operações somente leitura) sessionId?: string; // correlação de auditoria (ex: operações iniciadas de dentro do contexto da session) route: string; // trilha de auditoria (ex: "POST /file/write") workspaceCwd: string; // raiz do limite de confiança (trust boundary) }

originatorClientId é opcional — atualmente, rotas somente leitura como file read funcionam normalmente quando o header está ausente (clientId ?? undefined é passado para fsFactory.forRequest). Rotas de write validam a legitimidade apenas quando o clientId está presente.

Onde é construído: O route handler L1 / method handler /acp extrai dos headers/params da request e passa para o L2. O L2 apenas consome, não extrai o contexto HTTP por conta própria.


5. Redução e Renomeação do AcpSessionBridge

5.1 Métodos Movidos da Bridge

MétodoDestinoMecanismoMotivo
initWorkspaceworkspace.initWorkspaceMigração direta (puramente local)Corrige o FIXME (a bridge não usava fsFactory, pulando o trust gate / audit)
setWorkspaceToolEnabledworkspace.setToolEnabledMigração direta (puramente local)Puramente file I/O + event fan-out, comentários indicam claramente “no ACP roundtrip”
getWorkspaceMcpStatusworkspace.getMcpStatusvia callback queryWorkspaceStatusConsulta de status workspace-scoped
getWorkspaceSkillsStatusworkspace.getSkillsStatusvia callback queryWorkspaceStatusIdem
getWorkspaceProvidersStatusworkspace.getProvidersStatusvia callback queryWorkspaceStatusIdem
getWorkspaceEnvStatusworkspace.getEnvStatusvia callback queryWorkspaceStatusIdem
getWorkspacePreflightStatusworkspace.getPreflightStatusvia callback queryWorkspaceStatusIdem
restartMcpServerworkspace.restartMcpServervia callback invokeWorkspaceCommandMutação workspace-scoped

listWorkspaceSessions / recordHeartbeat / getHeartbeatState / updateSessionMetadata permanecem no bridge — eles acessam o mapa de sessões interno byId do bridge e são operações com escopo de sessão.

5.2 O que permanece no bridge

  • Todo o ciclo de vida de session/channel (spawn/load/resume/send/cancel/close/kill/detach)
  • Manutenção do EventBus + implementação de fan-out do publishWorkspaceEvent (para consumo pelos callbacks do service)
  • knownClientIds (para consumo pelos callbacks do service)
  • queryWorkspaceStatus / invokeWorkspaceCommand (novamente expostos, encapsulando channel + timeout + error, para delegação pelos callbacks do service)
  • Mediator de arbitragem de permissões
  • Alterações de configuração da session (model/approvalMode/recap)
  • Estado da session (context/supportedCommands/metadata/heartbeat/listSessions)

5.3 Renomeações

  • HttpAcpBridgeAcpSessionBridge
  • createHttpAcpBridgecreateAcpSessionBridge
  • Arquivo serve/httpAcpBridge.tsserve/acpSessionBridge.ts

Sem consumidores de pacotes externos (verificado que não há referências fora de packages/cli/src/serve/ e packages/acp-bridge/src/), seguro internamente.


6. Métodos ext northbound do /acp

6.1 Namespaces

qwen/workspace/... (para diferenciar do existente qwen/control/...):

  • qwen/control/... = daemon→child encaminhamento de comandos (southbound, via AcpSessionBridge)
  • qwen/workspace/... = daemon operações locais de workspace (northbound, terminando em DaemonWorkspaceService)

Aguardando confirmação de chiga0. Se o namespace for alterado, basta trocar o prefixo do nome do método, sem afetar a arquitetura.

6.2 Lista de métodos

methodREST correspondenteChamada L2
qwen/workspace/fs/readGET /file?path=...workspace.file.read(ctx, path)
qwen/workspace/fs/readBytesGET /file/bytes?path=...workspace.file.readBytes(ctx, path)
qwen/workspace/fs/writePOST /file/writeworkspace.file.write(ctx, path, content)
qwen/workspace/fs/editPOST /file/editworkspace.file.edit(ctx, path, edits)
qwen/workspace/fs/globGET /glob?pattern=...workspace.file.glob(ctx, pattern)
qwen/workspace/fs/listGET /list?path=...workspace.file.list(ctx, path)
qwen/workspace/fs/statGET /stat?path=...workspace.file.stat(ctx, path)
qwen/workspace/auth/startPOST /workspace/auth/device-flowworkspace.auth.startFlow(ctx)
qwen/workspace/auth/statusGET /workspace/auth/statusworkspace.auth.getAuthStatus(ctx)
qwen/workspace/auth/flowGET /workspace/auth/device-flow/:idworkspace.auth.getFlowStatus(ctx, flowId)
qwen/workspace/auth/cancelPOST /workspace/auth/device-flow/:id (cancel)workspace.auth.cancelFlow(ctx, flowId)
qwen/workspace/agents/listGET /workspace/agentsworkspace.agents.list(ctx)
qwen/workspace/agents/getGET /workspace/agents/:agentTypeworkspace.agents.get(ctx, agentType)
qwen/workspace/agents/createPOST /workspace/agentsworkspace.agents.create(ctx, spec)
qwen/workspace/agents/updatePOST /workspace/agents/:agentTypeworkspace.agents.update(ctx, agentType, spec)
qwen/workspace/agents/deleteDELETE /workspace/agents/:agentTypeworkspace.agents.delete(ctx, agentType)
qwen/workspace/memory/listGET /workspace/memoryworkspace.memory.list(ctx)
qwen/workspace/memory/readGET /workspace/memory/:keyworkspace.memory.read(ctx, key)
qwen/workspace/memory/writePOST /workspace/memoryworkspace.memory.write(ctx, key, content)
qwen/workspace/memory/deleteDELETE /workspace/memory/:keyworkspace.memory.delete(ctx, key)
qwen/workspace/initPOST /workspace/initworkspace.initWorkspace(ctx, opts)
qwen/workspace/tool/togglePOST /workspace/tool/toggleworkspace.setToolEnabled(ctx, toolName, enabled)
qwen/workspace/status/mcpGET /workspace/mcpworkspace.getMcpStatus()
qwen/workspace/status/skillsGET /workspace/skillsworkspace.getSkillsStatus()
qwen/workspace/status/providersGET /workspace/providersworkspace.getProvidersStatus()
qwen/workspace/status/envGET /workspace/envworkspace.getEnvStatus()
qwen/workspace/status/preflightGET /workspace/preflightworkspace.getPreflightStatus()
qwen/workspace/mcp/restartPOST /workspace/mcp/restartworkspace.restartMcpServer(ctx, serverName, opts)

Ao anunciar capabilities, declare esses métodos em _meta.qwen.methods.


7. Lista de alterações de arquivos

7.1 Novos

ArquivoPropósito
serve/workspace-service/types.tsWorkspaceRequestContext + interfaces de sub-service
serve/workspace-service/index.tsfacade factory
serve/workspace-service/fileService.tsImplementação do FileService
serve/workspace-service/authService.tsImplementação do AuthService
serve/workspace-service/agentsService.tsImplementação do AgentsService
serve/workspace-service/memoryService.tsImplementação do MemoryService
serve/workspace-service/__tests__/fileService.test.tsunit test
serve/workspace-service/__tests__/authService.test.tsunit test
serve/workspace-service/__tests__/agentsService.test.tsunit test
serve/workspace-service/__tests__/memoryService.test.tsunit test
serve/workspace-service/__tests__/e2e.test.tsValidação de equivalência E2E REST ↔ /acp

7.2 Modificados

ArquivoAlteração
acp-bridge/src/bridge.tsRemove 8 métodos de workspace (initWorkspace / setWorkspaceToolEnabled / 5 status getters / restartMcpServer); expõe novamente queryWorkspaceStatus + invokeWorkspaceCommand; renomeia a função factory
acp-bridge/src/bridgeTypes.tsRenomeia interface HttpAcpBridgeAcpSessionBridge; remove 8 assinaturas de métodos de workspace; adiciona assinaturas de queryWorkspaceStatus + invokeWorkspaceCommand
acp-bridge/src/bridgeOptions.tsAtualiza referências do JSDoc
acp-bridge/src/status.tsAtualiza nomes de classes nas mensagens de erro
cli/src/serve/httpAcpBridge.ts → renomeado acpSessionBridge.tsAtualiza re-export
cli/src/serve/runQwenServe.tsConstrói DaemonWorkspaceService, injeta callback, passa para routes e handler /acp
cli/src/serve/server.tsroutes mudam de conexão direta com fsFactory/DeviceFlowRegistry para chamar workspace.file.* / workspace.auth.*
cli/src/serve/workspaceAgents.tsLógica de negócio movida para agentsService.ts; arquivo original se torna um wrapper fino de route handler (constrói ctx → chama service)
cli/src/serve/workspaceMemory.tsIdem
cli/src/serve/routes/workspaceFileRead.tsIdem
cli/src/serve/routes/workspaceFileWrite.tsIdem
/acp handler (dentro de acp-integration/ ou serve/)Adiciona dispatch de método northbound

8. Compatibilidade do SDK e formato de erros

8.1 Compatibilidade retroativa do SDK

A superfície da API REST (caminhos, métodos HTTP, schema JSON de requisição/resposta) permanece inalterada. O DaemonClient / DaemonSessionClient no sdk-typescript não precisa de nenhuma alteração.

Método de validação: os packages/sdk-typescript/test/unit/DaemonClient.test.ts e DaemonSessionClient.test.ts existentes devem passar sem nenhuma modificação neste PR.

8.2 Formato de erro de rejeição do trust gate do /acp

Os dois transportes são semanticamente equivalentes, mas codificados de forma diferente:

CenárioREST/acp (JSON-RPC)
Bearer token inválido/ausente401 { error, code: "unauthorized" }{ error: { code: -32001, message: "unauthorized" } }
clientId inválido400 { error, code: "invalid_client_id" }{ error: { code: -32602, message: "invalid_client_id", data: {...} } }
Rejeição do trust gate (path traversal, etc.)403 { error, code: "forbidden" }{ error: { code: -32003, message: "forbidden", data: {...} } }

Os códigos de erro do JSON-RPC seguem o registro de códigos de erro do ACP  (a faixa padrão -32000 ~ -32099 é para erros de aplicativo definidos pelo servidor). Os valores de código específicos devem ser alinhados com a lógica de mapeamento de erros existente do /acp durante a implementação (acp-integration/errorCodes.ts).


9. Estratégia de testes

CamadaTipo de testeObjetivo de cobertura
Sub-service unitJest, mock de fsFactory / DeviceFlowRegistry / SubagentManager / callbacksCorretude da lógica de negócio + rejeição de clientId inválido pelo trust gate
Route integrationTestes de route existentes alterados para passar pelo service (valida que a superfície HTTP não muda)Garantia de regressão, caminhos REST não quebram
Validação de equivalência E2EInicia serve real + requisições HTTPREST e /acp retornam resultados equivalentes para a mesma operação; trust gate rejeita consistentemente em ambos os lados

Matriz de validação E2E

  • Leitura/escrita de arquivos: REST GET /file vs /acp qwen/workspace/fs/read → mesmo resultado
  • CRUD de Agent: REST POST /workspace/agents vs /acp qwen/workspace/agents/create → mesmo comportamento
  • Rejeição no trust gate: clientId inválido retorna 403 em ambos os caminhos
  • Inicialização do workspace: validar fluxo do fsFactory + geração de audit trail

10. Formato do PR

Commit atômico em um único PR, contendo:

  • Todos os novos arquivos do DaemonWorkspaceService
  • REST route handler alterado para chamar o service
  • Redução da bridge (extração de 8 métodos de workspace) + exposição de 2 novos métodos de delegação child
  • Renomeação de HttpAcpBridge para AcpSessionBridge
  • Adição de 27 novos métodos ext northbound de /acp
  • Testes completos (unit + integration + e2e)

11. O que não será feito (scope boundary)

  • EventBus com escopo de workspace (território do PR 24)
  • ClientRegistry com escopo de workspace (território do PR 24)
  • Separação L2 ↔ L3 (extração de ClientSideConnection da bridge)
  • Transformar REST em um shim de compatibilidade para /acp (direção de longo prazo)
  • Unificação do modo standalone de channels (questão de modelo de deploy independente)
  • Migração de listWorkspaceSessions / recordHeartbeat / getHeartbeatState / updateSessionMetadata (session-scoped, manter no local atual)
  • Transferência de ownership de publishWorkspaceEvent / knownClientIds (infraestrutura derivada de session, manter na bridge, service consome via callback)

12. Pontos de decisão pendentes de confirmação por chiga0

  1. Namespace northbound de /acp: qwen/workspace/... vs outros (como reutilizar qwen/control/...)
  2. Se a renomeação será no mesmo PR: tendência a manter no mesmo PR, mas pode ser separado conforme feedback

Se esses dois pontos precisarem de ajuste, afetarão apenas a nomenclatura e os limites do commit, não a arquitetura.

Last updated on