Arquitetura
Visão geral
Seção intitulada “Visão geral”O Control Center segue a Clean Architecture orientada a features, com o padrão de portas e adaptadores. A base de código é organizada em torno de domínios de negócio (as features), com regras de dependência rígidas aplicadas por um teste de arquitetura automatizado.
Um cliente fino sobre um servidor headless
Seção intitulada “Um cliente fino sobre um servidor headless”O Control Center tem uma arquitetura de cliente fino. Nenhum cliente abre o banco de dados — um processo headless do cc_server é o dono dos dados (um pequeno global.db mais um arquivo SQLite por espaço de trabalho, aberto sob demanda) e os serve por RPC sobre WebSocket. O app de desktop, a build web e o app complementar para telefone são todos renderizadores sobre essa única conexão RPC.
O desktop escolhe como chega ao seu servidor na primeira inicialização: local (ele inicia um cc_server nesta máquina e conversa por loopback — o padrão) ou remoto (ele se conecta a um cc_server em outro lugar por um WebSocket seguro). O cliente web é sempre remoto. O resolvedor de inicialização (bootstrap/server_backend.dart) escolhe o caminho antes de o Riverpod existir e depois substitui rpcClientProvider pelo cliente conectado — assim, cada feature lê e grava pelo servidor, e não em um banco de dados local.
A própria camada RPC é sem estado: uma sessão é autenticada, mas não está vinculada a um espaço de trabalho. Cada chamada com escopo de espaço de trabalho carrega o próprio workspace_id nos argumentos, e o dispatcher recusa uma chamada dessas sem ele. É isso que permite a dois clientes em um mesmo servidor manterem espaços de trabalho ativos diferentes sem um “workspace atual” mantido pelo servidor para discordar — e significa que o isolamento é aplicado por chamada, não por conexão.
Este é um workspace Dart de lockfile resolvido, com vinte e um membros — quinze pacotes mais seis apps — tendo o app de desktop+web (control_center) como raiz do workspace. Os seis apps são o binário do servidor headless (cc_server), o executor de frota headless (cc_worker), a PWA de telefone (cc_remote), o broker de relay WebSocket (cc_signaling_server), a galeria do Widgetbook (cc_gallery) e o host de demonstração público (cc_demo_server). Os quinze pacotes são cc_ui (design system), cc_domain (kernel compartilhado em Dart puro: entidades, objetos de valor, portas, eventos e a camada de domínio de cada feature), cc_harness (o kernel de agente embutido: um loop de agente em Dart puro — mensagens, porta de provedor, ferramentas, compactação, orientação, hooks, subagentes — sem dart:io e sem qualquer outra dependência cc_*), cc_harness_runtime (o adaptador de runtime do harness embutido: provedores de streaming exclusivos de VM, broker de credenciais OAuth/PKCE, armazenamentos de credenciais e o conjunto genérico de ferramentas — executa agentes sem nenhum CLI externo instalado), cc_rpc (transportes de cliente), cc_host (kernel do servidor), cc_data (repositórios remotos), cc_persistence (banco de dados do lado do servidor), cc_server_core (composição do app-servidor + runtime de identidade/presença/frota/avaliações), cc_infra (adaptadores exclusivos de VM), cc_mcp (ferramentas MCP), cc_mcp_client (cliente para servidores MCP externos, que conecta as ferramentas deles ao registro local), cc_markdown (mecanismo de markdown do repositório), cc_natives (folha FFI) e system_audio_capture. Consulte Implantação e clientes para ver a matriz completa de clientes e o que roda onde.
Regra de dependências
Seção intitulada “Regra de dependências”Presentation → Application/Providers → Domain ← Infrastructure- Camada de domínio: entidades, objetos de valor, interfaces de repositório, portas e serviços de domínio em Dart puro. Zero importações de infraestrutura (nada de dio, drift ou modelos de rede).
- Camada de apresentação: telas, widgets, notifiers. Nenhum acesso direto a drift/DAO/camada de dados; tudo passa por providers do Riverpod → repositórios.
- Camada de infraestrutura: implementações concretas das portas de domínio e das interfaces de repositório. Depende do domínio, nunca o contrário.
Estrutura de features
Seção intitulada “Estrutura de features”O formato completo de uma feature é:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersPouquíssimas features em lib/features/ realmente têm as quatro pastas, e essas lacunas são informativas, não acidentais. O kernel compartilhado absorveu o domain/ de cada feature dentro de cc_domain, e o cliente fino não contém implementações de repositório, então a maioria das features em lib/ tem apenas presentation/ e providers/, e nada mais. Um diretório data/ ou domain/ que sobrevive no cliente indica algo genuinamente do lado do cliente.
Duas exceções conhecidas merecem atenção:
mcpsó tem providers. Sua interface de configurações e status fica emsettings/, e a superfície de ferramentas em si é um pacote (cc_mcp), não uma pasta de feature.orchestrationeplan_studiotêm apenaspresentation/eproviders/.
A feature guardrails coloca sua camada de domínio em cc_domain/features/guardrails/ em vez de no app raiz, porque é o servidor que a aplica: uma regra mapeia um ActionClass (ou um commandPrefix de shell) para uma decisão — allow, prompt ou deny — em um escopo; as regras são armazenadas por espaço de trabalho, e toda ação de agente governada é resolvida pela cadeia de políticas antes de ser executada. A resolução percorre space > agent > workspace > mode preset > built-in default e o primeiro escopo com uma regra correspondente decide; o mais restritivo é apenas um critério de desempate dentro de um mesmo escopo e para combinar as várias classes que uma ação declara. A superfície voltada ao operador é Configurações → Espaço de trabalho → Permissões dos agentes, em /workspaces/<id>/settings/workspace/permissions. Consulte Barreiras para conhecer o modelo.
Kernel compartilhado
Seção intitulada “Kernel compartilhado”packages/cc_domain/lib/core/domain/ contém entidades e repositórios compartilhados por três ou mais features. É Dart puro, com zero dependências de infraestrutura, e é isso que permite que tanto o cliente Flutter quanto o binário de servidor sem Flutter importem as mesmas definições:
- Entidades principais:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Identidade e multijogador:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Entidades de memória:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Objetos de valor compartilhados:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Portas compartilhadas:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Serviços de domínio:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ tipos de evento (workspace/agente, PR/revisão, mensagens, tickets, pipeline, orquestração, memória, calendário/reuniões, identidade e associação, observabilidade)
Identidade e multijogador
Seção intitulada “Identidade e multijogador”O Control Center é multiusuário: humanos e agentes são atores em pé de igualdade, unificados por um Principal. Um membro de espaço de trabalho é um User vinculado a um Workspace com um WorkspaceRole (owner/admin/member/viewer/guest). A associação é a fronteira de acesso — não uma chave de emparelhamento — e as concessões por repo impedem que a associação ao espaço de trabalho conceda mais privilégios do que o forge. O primeiro usuário criado em um servidor novo é dono da instalação; os demais entram por convite ou OIDC. Criar um espaço de trabalho marca esse usuário como owner.
A colaboração em tempo real usa servidor autoritativo + last-writer-wins (LWW) por campo, não um CRDT. A presença (quem está aqui, onde está, cursores, digitação, o status ao vivo de um agente e o custo em execução) é uma via efêmera que nunca é persistida; o estado durável trafega por uma base de mutação otimista + rebase no servidor + LWW. Humanos e agentes compartilham uma única lista de participantes. Com um único operador humano, a via de presença fica ociosa e nenhum elemento de interface da lista aparece — o multijogador não custa nada ao usuário solo.
Portas e adaptadores
Seção intitulada “Portas e adaptadores”As portas são interfaces abstratas na camada de domínio. Os adaptadores são implementações concretas na infraestrutura. Como o servidor headless é o dono dos dados e precisa ser compilado sem Flutter, quase todos os adaptadores são exclusivos de VM e ficam em packages/cc_infra/lib/src/, organizados por assunto:
| Porta | Localização do adaptador |
|---|---|
SandboxPort |
cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart) |
CredentialBrokerPort |
cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, github_fine_grained_broker.dart) |
GitRepoInspectorPort |
cc_infra/lib/src/git/ (git_repo_inspector.dart) |
EmbeddingPort |
cc_infra/lib/src/embedding/ (embedding_service.dart) |
NotificationPort |
lib/core/notifications/ (lado do cliente: renderiza eventos do servidor localmente) |
AgentBackend |
cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart) |
TicketProviderPort |
cc_infra/lib/src/tickets/linear/ (LinearTicketAdapter). Jira, ClickUp e GitHub Issues são adaptadores de sincronização de tickets em cc_infra/lib/src/tickets/sync/; tickets locais ficam no banco de dados do espaço de trabalho, sem TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (um serviço de domínio em Dart puro — não precisa de infraestrutura) |
A raiz de composição do app (di/providers.dart) vincula as portas do lado do cliente às implementações por meio de providers do Riverpod; o servidor compõe seus adaptadores em cc_server_core.
Gerenciamento de estado
Seção intitulada “Gerenciamento de estado”Riverpod para todo o estado do cliente:
Notifier<T>eAsyncNotifier<T>para estado mutávelFutureProvider<T>para dados assíncronos- Listas e linhas ao vivo chegam por assinaturas RPC como
AsyncValue<List<T>>— o servidor usa.watch()do Drift internamente; nenhum cliente abre o Drift - As ferramentas MCP recebem dependências como parâmetros tipados de construtor, nunca
Ref
Banco de dados
Seção intitulada “Banco de dados”Drift (SQLite), dividido em dois bancos de dados, ambos em cc_persistence:
GlobalDatabase(<dataDir>/global.db) — uma lista curta de tabelas genuinamente globais do servidor: o registro de espaços de trabalho, usuários/preferências/dispositivos emparelhados, conexões de SSO, configurações de servidor de toda a instalação, políticas de ação gerenciadas (um limite de toda a instalação que só pode restringir as barreiras dos espaços de trabalho), o feed de notícias por usuário, a fila da frota (workers/jobs/log de alocação), o índiceworkspace_routesde pré-autenticação e metadados da instalação. A inicialização abre apenas este arquivo, então o custo de partida permanece constante por mais histórico que os espaços de trabalho acumulem. Adicionar uma tabela aqui é uma decisão de isolamento que um teste de catraca obriga você a justificar.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — um diretório por espaço de trabalho, contendo um arquivo SQLite mais tudo o mais que pertence apenas àquele espaço de trabalho, de modo que os worktrees e arquivos de agente de uma conversa ficam ao lado do banco de dados e são excluídos junto com ele. Quase tudo mora aqui: agentes, espaços, tickets, memória, pipelines, reuniões, o grafo de código, revisões, repos. Os arquivos são abertos sob demanda no primeiro acesso, por meio deWorkspaceDatabaseManager.of(workspaceId), e nesse momento pagam seu próprioquick_check, a instalação de FTS/triggers e ovector_init.
As duas metades começaram de uma base v1 consolidada — o onCreate constrói o esquema atual — com etapas de migração acrescentadas apenas quando um banco de dados já implantado precisa ser levado adiante.
Como um espaço de trabalho é um arquivo, exportar um espaço de trabalho é uma única instrução VACUUM INTO (workspace.export), e não um dump tabela por tabela; workspace.import adota esse arquivo e usa o workspace_meta embutido para distinguir se o próprio arquivo foi readotado ou se veio de outra instalação.
Ambos os bancos de dados pertencem a cc_persistence e são abertos somente pelo cc_server; nenhum cliente abre um BD — todo cliente lê e grava por RPC. As tabelas são definidas em packages/cc_persistence/lib/database/tables/, os DAOs em .../daos/; as entidades de domínio são Dart puro, separadas das classes de tabela do Drift, com o mapeamento nas camadas de dados. O FTS5 cuida da busca em texto completo e o sqlite_vector dos embeddings, ambos exclusivos do servidor; os dois bancos de dados rodam PRAGMA foreign_keys=ON em modo de journal WAL.
Roteamento
Seção intitulada “Roteamento”go_router, com um fato que governa todo o resto: todo destino dentro do app tem o prefixo do espaço de trabalho, no formato /workspaces/:workspaceId/…. O id do espaço de trabalho na URL é a única fonte de verdade do espaço de trabalho ativo — o activeWorkspaceIdProvider é determinado pela rota, e não o contrário — por isso ler dados com escopo de espaço de trabalho fora de uma rota com prefixo não tem de onde tirar o id.
- Funções construtoras de rota em
router/routes.dart, cada uma recebendo o id do espaço de trabalho como primeiro argumento, e não constantes de string - Quatro rotas anteriores ao contexto não têm prefixo:
/splash,/onboarding,/signed-oute/workspaces(o seletor) - O
ShellRouteenvolve o shell do app; essas quatro superfícies são renderizadas em tela cheia fora dele - Guarda de autenticação: uma configuração incompleta vai para
/onboarding; uma configuração concluída cuja credencial de forge sumiu vai para/signed-out. Concluída significa que pelo menos um forge está conectado para o usuário autenticado e que existe pelo menos um espaço de trabalho — não especificamente o GitHub — e o CLIghnão é um método de autenticação - Portão de onboarding em
features/auth/providers/
Rede (lado do servidor)
Seção intitulada “Rede (lado do servidor)”Todo HTTP externo vive no servidor. Os clientes nunca se conectam diretamente ao GitHub, ao Linear ou ao Google — eles chamam operações RPC do servidor, e até mídia remota é obtida pelo endpoint /proxy/media do servidor, e não da origem upstream.
- Clientes dio em
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClienteGoogleCalendarApiClient - Injeção do token de autenticação por interceptadores, incluindo um interceptador de OAuth do Google por conta em
cc_server_coreque renova os tokens em respostas 401 - Todos os erros são mapeados por
packages/cc_infra/lib/src/network/error_mapper.dart→ subclasses tipadas deAppExceptiondefinidas emcc_domain
Segurança
Seção intitulada “Segurança”O servidor guarda as credenciais que importam: tokens do GitHub e dos sistemas de tickets para lançamentos de agentes, chaves de provedores de LLM e tokens OAuth ficam todos sob o diretório de dados do servidor ou em seu ambiente. O que o keychain do cliente ainda guarda é a própria credencial do dispositivo e a chave de emparelhamento de um servidor remoto.
- Segredos em
flutter_secure_storage(keychain do macOS, gerenciador de credenciais do Windows, libsecret do Linux) — a credencial de dispositivo do cliente e as chaves de emparelhamento remoto, não tokens de provedores shared_preferencesapenas para preferências não sensíveis (tema, fonte, layout)- Segredos de provedor remanescentes de instalações mais antigas são entregues ao servidor uma única vez por
features/auth/providers/credential_migration.darte depois excluídos do keychain do cliente
Aplicação da arquitetura
Seção intitulada “Aplicação da arquitetura”As restrições de arquitetura são validadas por test/core/architecture_constraints_test.dart, que falha se as regras de dependência forem violadas.
Conceitos relacionados
Seção intitulada “Conceitos relacionados”- Espaços de trabalho e isolamento: como o isolamento é aplicado
- Ciclo de vida do despacho de agentes: o fluxo de despacho
- Eventos de domínio: comunicação entre features
- Implantação e clientes: o modelo de servidor de cliente fino e a matriz de clientes