Arquitetura
Visão geral
Secção intitulada “Visão geral”O Control Center segue a Clean Architecture orientada a funcionalidades, com um padrão de portas e adaptadores. A base de código está organizada em torno de domínios de negócio (as funcionalidades), com regras de dependência estritas impostas por um teste de arquitetura automatizado.
Um cliente fino sobre um servidor headless
Secção intitulada “Um cliente fino sobre um servidor headless”O Control Center tem uma arquitetura de cliente fino. Nenhum cliente abre a base de dados — um processo headless cc_server é o dono dos dados (um pequeno global.db mais um ficheiro SQLite por espaço de trabalho, abertos de forma preguiçosa) e serve-os por RPC sobre WebSocket. A aplicação para computador, a versão web e a aplicação complementar para telemóvel são todas renderizadores sobre essa única ligação RPC.
A aplicação para computador escolhe como chega ao seu servidor no primeiro arranque: local (lança um cc_server nesta máquina e comunica por loopback — a predefinição) ou remoto (liga-se a um cc_server noutro local através de um WebSocket seguro). O cliente web é sempre remoto. O resolvedor de arranque (bootstrap/server_backend.dart) escolhe o caminho antes de o Riverpod existir e, de seguida, substitui rpcClientProvider pelo cliente ligado — por isso cada funcionalidade lê e escreve através do servidor, e não de uma base 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 âmbito de espaço de trabalho transporta o seu próprio workspace_id nos argumentos, e o despachante recusa uma chamada dessas sem ele. É isso que permite a dois clientes num mesmo servidor manterem espaços de trabalho ativos diferentes sem um “espaço de trabalho atual” detido pelo servidor sobre o qual possam discordar — e significa que o isolamento é imposto por chamada, não por ligação.
Este é um único Dart workspace com lockfile resolvido, de vinte e um membros — quinze pacotes mais seis aplicações — tendo a aplicação para computador e web (control_center) como raiz do espaço de trabalho. As seis aplicações são o binário do servidor headless (cc_server), o executor headless da frota (cc_worker), a PWA de telemóvel (cc_remote), o broker de retransmissão WebSocket (cc_signaling_server), a galeria do Widgetbook (cc_gallery) e o anfitrião de demonstração público (cc_demo_server). Os quinze pacotes são cc_ui (sistema de design), cc_domain (kernel partilhado em Dart puro: entidades, objetos de valor, portas, eventos e a camada de domínio de cada funcionalidade), cc_harness (o kernel de agente integrado: um ciclo de agente em Dart puro — mensagens, porta de fornecedor, 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 integrado: fornecedores de streaming exclusivos de VM, intermediação 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 (base de dados do lado do servidor), cc_server_core (composição aplicação-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 liga as suas ferramentas ao registo local), cc_markdown (motor de markdown do repositório), cc_natives (folha FFI) e system_audio_capture. Consulte Implementação e clientes para ver a matriz completa de clientes e o que é executado onde.
Regra de dependências
Secçã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: ecrãs, 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 funcionalidades
Secção intitulada “Estrutura de funcionalidades”O formato completo de uma funcionalidade é:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersMuito poucas funcionalidades em lib/features/ têm, na realidade, as quatro pastas, e essas lacunas são informativas, não acidentais. O kernel partilhado absorveu o domain/ de cada funcionalidade em cc_domain, e o cliente fino não contém implementações de repositório, pelo que a maioria das funcionalidades em lib/ tem apenas presentation/ e providers/, e nada mais. Um diretório data/ ou domain/ que sobreviva no cliente assinala algo genuinamente do lado do cliente.
Duas exceções com nome merecem atenção:
mcpsó tem providers. A sua interface de definições e de estado fica emsettings/, e a própria superfície de ferramentas é um pacote (cc_mcp), não uma pasta de funcionalidade.orchestrationeplan_studiotêm apenaspresentation/eproviders/.
A funcionalidade guardrails coloca a sua camada de domínio em cc_domain/features/guardrails/ em vez de na aplicação 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 — num determinado âmbito, as regras são guardadas por espaço de trabalho e cada ação de agente regulada é 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 âmbito com uma regra correspondente decide; o mais restritivo é apenas um critério de desempate dentro de um mesmo âmbito e para combinar as várias classes que uma ação declara. A superfície visível ao operador é Definições → Espaço de trabalho → Permissões dos agentes, em /workspaces/<id>/settings/workspace/permissions. Consulte Barreiras para conhecer o modelo.
Kernel partilhado
Secção intitulada “Kernel partilhado”packages/cc_domain/lib/core/domain/ contém entidades e repositórios partilhados por três ou mais funcionalidades. É Dart puro, com zero dependências de infraestrutura, e é isso que permite que tanto o cliente Flutter como 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 partilhados:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Portas partilhadas:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Serviços de domínio:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ tipos de evento (espaço de trabalho/agente, PR/revisão, mensagens, tickets, pipeline, orquestração, memória, calendário/reuniões, identidade e pertença, observabilidade)
Identidade e multijogador
Secçã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 um espaço de trabalho é um User associado a um Workspace com um WorkspaceRole (owner/admin/member/viewer/guest). A pertença é a fronteira de acesso — não uma chave de emparelhamento — e as concessões por repositório impedem que a pertença ao espaço de trabalho conceda mais privilégios do que a forge. O primeiro utilizador criado num servidor novo é dono da instalação; os restantes entram por convite ou OIDC. Criar um espaço de trabalho marca esse utilizador como owner.
A colaboração em tempo real é servidor autoritativo + última escrita vence (LWW) por campo, não um CRDT. A presença (quem está aqui, onde está, cursores, quem está a escrever, o estado em direto e o custo em curso de um agente) é um canal efémero separado que nunca é persistido; o estado duradouro assenta numa espinha dorsal de mutação otimista + rebase no servidor + LWW. Humanos e agentes partilham uma única lista de participantes. Com um único operador humano, o canal de presença fica inativo e não aparece qualquer elemento de interface da lista de participantes — o multijogador não custa nada ao utilizador individual.
Portas e adaptadores
Secçã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 é dono dos dados e tem de ser compilado sem Flutter, quase todos os adaptadores são exclusivos de VM e ficam em packages/cc_infra/lib/src/, organizados por área:
| 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/ (do 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/; os tickets locais ficam na base de dados do espaço de trabalho, sem qualquer 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 da aplicação (di/providers.dart) liga as portas do lado do cliente às implementações através de providers do Riverpod; o servidor compõe os seus adaptadores em cc_server_core.
Gestão de estado
Secção intitulada “Gestão 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 em direto chegam por subscrições RPC como
AsyncValue<List<T>>— o servidor usa o.watch()do Drift internamente; nenhum cliente abre o Drift - As ferramentas MCP recebem as dependências como parâmetros tipados de construtor, nunca
Ref
Base de dados
Secção intitulada “Base de dados”Drift (SQLite), dividido em duas bases de dados, ambas em cc_persistence:
GlobalDatabase(<dataDir>/global.db) — uma lista curta de tabelas genuinamente globais ao servidor: o registo de espaços de trabalho, utilizadores/preferências/dispositivos emparelhados, ligações SSO, definições de servidor de toda a instalação, políticas de ação geridas (um limite de toda a instalação que só pode restringir as barreiras dos espaços de trabalho), o feed de notícias por utilizador, a fila da frota (workers/jobs/registo de colocação), o índiceworkspace_routesde pré-autenticação e os metadados da instalação. O arranque abre apenas este ficheiro, pelo que o custo de arranque se mantém constante por muito histórico que os espaços de trabalho acumulem. Adicionar uma tabela aqui é uma decisão de isolamento que um teste de catraca o obriga a justificar.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — um diretório por espaço de trabalho, que contém um ficheiro SQLite mais tudo o resto que pertence apenas a esse espaço de trabalho, de modo que os worktrees e os ficheiros de agente de uma conversa ficam ao lado da base de dados e são eliminados com ela. Quase tudo vive aqui: agentes, espaços, tickets, memória, pipelines, reuniões, o grafo de código, revisões, repos. Os ficheiros são abertos de forma preguiçosa no primeiro acesso, através deWorkspaceDatabaseManager.of(workspaceId), e pagam nessa altura o seu próprioquick_check, a instalação de FTS/triggers e ovector_init.
As duas metades começaram a partir de uma base v1 consolidada — o onCreate constrói o esquema atual —, com passos de migração acrescentados apenas quando uma base de dados já implementada tem de ser levada adiante.
Como um espaço de trabalho é um ficheiro, exportar um espaço de trabalho é uma única instrução VACUUM INTO (workspace.export), e não um dump tabela a tabela; workspace.import adota esse ficheiro e usa o workspace_meta incorporado para distinguir se o próprio ficheiro foi readotado ou se veio de outra instalação.
Ambas as bases de dados pertencem a cc_persistence e são abertas apenas pelo cc_server; nenhum cliente abre uma base de dados — todos os clientes leem e escrevem 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 trata da pesquisa de texto integral e o sqlite_vector dos embeddings, ambos exclusivos do servidor; ambas as bases de dados executam PRAGMA foreign_keys=ON em modo de journal WAL.
Encaminhamento
Secção intitulada “Encaminhamento”go_router, com um facto que governa todo o resto: todos os destinos dentro da aplicação têm o prefixo do espaço de trabalho, no formato /workspaces/:workspaceId/…. O id do espaço de trabalho no URL é a única fonte de verdade do espaço de trabalho ativo — o activeWorkspaceIdProvider é determinado pela rota, e não o contrário — e é por isso que ler dados com âmbito de espaço de trabalho fora de uma rota com prefixo não tem de onde obter o id.
- Funções construtoras de rotas 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 da aplicação; essas quatro superfícies são renderizadas em ecrã inteiro fora dele - Guarda de autenticação: uma configuração incompleta vai para
/onboarding; uma configuração concluída cuja credencial de forge desapareceu vai para/signed-out. Concluída significa que pelo menos uma forge está ligada para o utilizador com sessão iniciada 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 (do lado do servidor)
Secção intitulada “Rede (do lado do servidor)”Todo o HTTP externo vive no servidor. Os clientes nunca se ligam diretamente ao GitHub, ao Linear ou ao Google — chamam operações RPC do servidor, e até os conteúdos multimédia remotos são obtidos através do endpoint /proxy/media do servidor, e não da origem a montante.
- Clientes dio em
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClienteGoogleCalendarApiClient - Injeção do token de autenticação por interceptores, incluindo um interceptor de OAuth do Google por conta em
cc_server_coreque renova os tokens em respostas 401 - Todos os erros são mapeados através de
packages/cc_infra/lib/src/network/error_mapper.dart→ subclasses tipadas deAppExceptiondefinidas emcc_domain
Segurança
Secçã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 fornecedores de LLM e tokens OAuth ficam todos sob o diretório de dados do servidor ou no seu ambiente. O que o keychain do cliente ainda guarda é a sua própria credencial de dispositivo e a chave de emparelhamento de um servidor remoto.
- Segredos em
flutter_secure_storage(keychain do macOS, gestor de credenciais do Windows, libsecret do Linux) — a credencial de dispositivo do cliente e as chaves de emparelhamento remoto, não tokens de fornecedores shared_preferencesapenas para preferências não sensíveis (tema, tipo de letra, layout)- Segredos de fornecedor remanescentes de instalações mais antigas são entregues ao servidor uma única vez por
features/auth/providers/credential_migration.darte depois eliminados do keychain do cliente
Aplicação da arquitetura
Secçã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
Secção intitulada “Conceitos relacionados”- Espaços de trabalho e isolamento: como o isolamento é imposto
- Ciclo de vida do despacho de agentes: o fluxo de despacho
- Eventos de domínio: comunicação entre funcionalidades
- Implementação e clientes: o modelo de servidor de cliente fino e a matriz de clientes