Arquitectura
Resumen
Sección titulada «Resumen»Control Center sigue Clean Architecture centrada en funcionalidades con un patrón de puertos y adaptadores. El código se organiza en torno a dominios de negocio (funcionalidades), con reglas de dependencia estrictas que hace cumplir un test de arquitectura automatizado.
Un cliente ligero sobre un servidor headless
Sección titulada «Un cliente ligero sobre un servidor headless»Control Center es una arquitectura de cliente ligero. Ningún cliente abre la
base de datos: un proceso headless cc_server es el propietario de los datos
(un pequeño global.db más un archivo SQLite por espacio de trabajo, abiertos de
forma perezosa) y los sirve por RPC sobre WebSocket. La aplicación de escritorio,
la versión web y el complemento para el teléfono son todos renderizadores sobre
esa única conexión RPC.
El escritorio elige cómo llega a su servidor en el primer arranque: local
(lanza un cc_server en esta computadora y se comunica por loopback —el valor
predeterminado—) o remoto (se conecta a un cc_server en otro lugar por un
WebSocket seguro). El cliente web siempre es remoto. El resolutor de arranque
(bootstrap/server_backend.dart) elige la ruta antes de que exista Riverpod y
después reemplaza rpcClientProvider por el cliente conectado, de modo que cada
funcionalidad lee y escribe a través del servidor en lugar de hacerlo en una base
de datos local.
La propia capa RPC es sin estado: una sesión se autentica, pero no está
vinculada a un espacio de trabajo. Toda llamada con ámbito de espacio de trabajo
lleva su propio workspace_id en sus argumentos, y el despachador rechaza una
llamada así si no lo lleva. Eso es lo que permite que dos clientes de un mismo
servidor mantengan espacios de trabajo activos distintos sin un «espacio de
trabajo actual» alojado en el servidor con el que puedan discrepar, y significa
que el aislamiento se aplica por llamada, no por conexión.
Este es un único workspace de Dart con lockfile resuelto de veintiún miembros
—quince paquetes más seis aplicaciones—, con la aplicación de escritorio y web
(control_center) como raíz del workspace. Las seis aplicaciones son el binario
del servidor headless (cc_server), el ejecutor headless de flota (cc_worker),
la PWA del teléfono (cc_remote), el broker de retransmisión WebSocket
(cc_signaling_server), la galería de Widgetbook (cc_gallery) y el host de
demostración público (cc_demo_server). Los quince paquetes son cc_ui (sistema
de diseño), cc_domain (kernel compartido en Dart puro: entidades, objetos de
valor, puertos, eventos y la capa de dominio de cada funcionalidad), cc_harness
(el kernel de agente integrado: un bucle de agente en Dart puro —mensajes, puerto
de proveedor, herramientas, compactación, dirección, hooks, subagentes— sin
dart:io ni ninguna otra dependencia de cc_*), cc_harness_runtime (el
adaptador de tiempo de ejecución del harness integrado: proveedores de streaming
solo para la VM, intermediación de credenciales OAuth/PKCE, almacenes de
credenciales y el conjunto genérico de herramientas —ejecuta agentes sin tener
instalado ningún CLI externo—), cc_rpc (transportes de cliente), cc_host
(kernel del servidor), cc_data (repositorios remotos), cc_persistence (base
de datos del lado del servidor), cc_server_core (composición de la app y el
servidor + tiempo de ejecución de identidad/presencia/flota/evaluaciones),
cc_infra (adaptadores solo para la VM), cc_mcp (herramientas MCP),
cc_mcp_client (cliente para servidores MCP externos, que conecta sus
herramientas con el registro local), cc_markdown (motor de markdown integrado
en el repositorio), cc_natives (hoja FFI) y system_audio_capture. Consulta
Implementación y clientes para ver la
matriz completa de clientes y qué se ejecuta dónde.
Regla de dependencias
Sección titulada «Regla de dependencias»Presentation → Application/Providers → Domain ← Infrastructure- Capa de dominio: entidades, objetos de valor, interfaces de repositorio, puertos y servicios de dominio en Dart puro. Cero importaciones de infraestructura (ni dio, ni drift, ni modelos de red).
- Capa de presentación: pantallas, widgets, notificadores. Sin acceso directo a drift/DAO/capa de datos; todo pasa por los proveedores de Riverpod → repositorios.
- Capa de infraestructura: implementaciones concretas de los puertos de dominio y las interfaces de repositorio. Depende del dominio, nunca al revés.
Estructura de las funcionalidades
Sección titulada «Estructura de las funcionalidades»La forma completa de una funcionalidad es:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersMuy pocas funcionalidades bajo lib/features/ llevan realmente las cuatro, y
esos huecos son informativos en lugar de accidentales. El kernel compartido
absorbió el domain/ de cada funcionalidad en cc_domain, y el cliente ligero
no contiene implementaciones de repositorio, así que la mayoría de las
funcionalidades bajo lib/ son presentation/ más providers/ y nada más. Un
directorio data/ o domain/ que sobreviva en el cliente marca algo
genuinamente del lado del cliente.
Hay dos excepciones con nombre propio que conviene conocer:
mcpsolo tiene proveedores. Su interfaz de ajustes y estado vive ensettings/, y la propia superficie de herramientas es un paquete (cc_mcp), no una carpeta de funcionalidad.orchestrationyplan_studiosolo llevanpresentation/yproviders/.
La funcionalidad guardrails coloca su capa de dominio en
cc_domain/features/guardrails/ en lugar de en la aplicación raíz, porque el
servidor es quien la aplica: una regla asigna un ActionClass (o un
commandPrefix de shell) a una decisión —allow, prompt o deny— en un
ámbito; las reglas se guardan por espacio de trabajo y toda acción de agente
gobernada se resuelve por la cadena de políticas antes de ejecutarse. La
resolución recorre space > agent > workspace > mode preset > built-in default y
decide el primer ámbito con una regla coincidente; lo más restrictivo solo es
un desempate dentro de un ámbito y para combinar las varias clases que declara
una acción. La superficie de cara al operador es Ajustes → Espacio de trabajo →
Permisos de agentes, en /workspaces/<id>/settings/workspace/permissions.
Consulta Barandillas para conocer el
modelo.
Kernel compartido
Sección titulada «Kernel compartido»packages/cc_domain/lib/core/domain/ contiene entidades y repositorios
compartidos por 3 o más funcionalidades. Es Dart puro con cero dependencias de
infraestructura, que es lo que permite que tanto el cliente de Flutter como el
binario de servidor sin Flutter importen las mismas definiciones:
- Entidades principales:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Identidad y multijugador:
User,Principal(UserPrincipal|AgentPrincipalsellados),WorkspaceMember,WorkspaceRole - Entidades de memoria:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Objetos de valor compartidos:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Puertos compartidos:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Servicios de dominio:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ tipos de evento (espacio de trabajo/agente, PR/revisión, mensajería, tickets, pipeline, orquestación, memoria, calendario/reuniones, identidad y membresía, observabilidad)
Identidad y multijugador
Sección titulada «Identidad y multijugador»Control Center es multiusuario: las personas y los agentes son actores en pie de
igualdad unificados por un Principal. Un miembro de un espacio de trabajo es un
User vinculado a un Workspace con un WorkspaceRole
(owner/admin/member/viewer/guest). La membresía es la frontera de
acceso —no una clave de emparejamiento—, y las concesiones por repositorio
impiden que la membresía en un espacio de trabajo otorgue por sí sola más
privilegios que el host de código. El primer usuario creado en un servidor nuevo
es el propietario de la instalación; los demás se incorporan por invitación u
OIDC. Al crear un espacio de trabajo, ese usuario queda marcado como owner.
La colaboración en tiempo real es servidor autoritativo + última escritura gana (LWW) por campo, no un CRDT. La presencia (quién está aquí, dónde está, cursores, escritura, el estado en vivo de un agente y su costo de ejecución) es un carril efímero aparte que nunca se persiste; el estado duradero se apoya en una columna vertebral de mutación optimista + rebase en el servidor + LWW. Las personas y los agentes comparten un mismo listado. Con un único operador humano, el carril de presencia queda inactivo y no aparece ningún elemento de interfaz del listado: el multijugador no le cuesta nada al usuario individual.
Puertos y adaptadores
Sección titulada «Puertos y adaptadores»Los puertos son interfaces abstractas de la capa de dominio. Los adaptadores son
implementaciones concretas en la infraestructura. Como el servidor headless es el
propietario de los datos y tiene que compilarse sin Flutter, casi todos los
adaptadores son solo para la VM y viven en packages/cc_infra/lib/src/,
organizados por asunto:
| Puerto | Ubicación del 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/ (del lado del cliente: representa los eventos del servidor en local) |
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 y GitHub Issues son adaptadores de sincronización de tickets en cc_infra/lib/src/tickets/sync/; los tickets locales viven en la base de datos del espacio de trabajo sin ningún TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (un servicio de dominio en Dart puro: no necesita infraestructura) |
La raíz de composición de la aplicación (di/providers.dart) vincula los puertos
del lado del cliente con sus implementaciones mediante proveedores de Riverpod;
el servidor compone sus adaptadores en cc_server_core.
Administración del estado
Sección titulada «Administración del estado»Riverpod para todo el estado del cliente:
Notifier<T>yAsyncNotifier<T>para estado mutableFutureProvider<T>para datos asíncronos- Las listas y las filas en vivo llegan por suscripciones RPC como
AsyncValue<List<T>>: el servidor usa Drift.watch()internamente; ningún cliente abre Drift - Las herramientas MCP reciben sus dependencias como parámetros de constructor
tipados, nunca como
Ref
Base de datos
Sección titulada «Base de datos»Drift (SQLite), dividida en dos bases de datos, ambas en cc_persistence:
GlobalDatabase(<dataDir>/global.db) — una lista corta de tablas genuinamente de todo el servidor: el registro de espacios de trabajo, usuarios/preferencias/dispositivos emparejados, conexiones SSO, ajustes del servidor de toda la instalación, políticas de acciones gestionadas (un límite de toda la instalación que solo puede endurecer las barandillas del espacio de trabajo), el feed de noticias por usuario, la cola de la flota (workers/jobs/registro de colocación), el índiceworkspace_routesprevio a la autenticación y los metadatos de la instalación. El arranque abre solo este archivo, así que el costo de inicio se mantiene plano por mucha historia que acumulen los espacios de trabajo. Agregar una tabla aquí es una decisión de aislamiento que un test de trinquete te obliga a justificar.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — un directorio por espacio de trabajo, que contiene un archivo SQLite más todo lo demás que pertenece solo a ese espacio de trabajo, de modo que los worktrees y los archivos de agente de una conversación quedan junto a la base de datos y se eliminan con ella. Aquí vive casi todo: agentes, espacios, tickets, memoria, pipelines, reuniones, el grafo de código, revisiones, repos. Los archivos se abren de forma perezosa en el primer acceso medianteWorkspaceDatabaseManager.of(workspaceId)y entonces pagan su propioquick_check, la instalación de FTS/disparadores yvector_init.
Ambas mitades partieron de una base v1 aplanada —onCreate construye el esquema
actual—, con pasos de migración agregados solo cuando hay que trasladar hacia
adelante una base de datos ya desplegada.
Como un espacio de trabajo es un solo archivo, exportar un espacio de trabajo es
una sola sentencia VACUUM INTO (workspace.export), no un volcado tabla por
tabla; workspace.import adopta un archivo así y usa el workspace_meta
incorporado para distinguir su propio archivo readoptado de uno que vino de otra
instalación.
El propietario de ambas bases de datos es cc_persistence, y solo las abre
cc_server; ningún cliente abre una base de datos: todos los clientes leen y
escriben por RPC. Las tablas se definen en
packages/cc_persistence/lib/database/tables/, los DAO en .../daos/; las
entidades de dominio son Dart puro, separadas de las clases de tabla de Drift,
con el mapeo en las capas de datos. FTS5 se encarga de la búsqueda de texto
completo y sqlite_vector de los embeddings, ambos solo en el servidor; ambas
bases de datos ejecutan PRAGMA foreign_keys=ON en modo de diario WAL.
Enrutamiento
Sección titulada «Enrutamiento»go_router, con un hecho que gobierna el resto: todo destino dentro de la
aplicación lleva el prefijo del espacio de trabajo como
/workspaces/:workspaceId/…. El id del espacio de trabajo en la URL es la única
fuente de verdad del espacio de trabajo activo —activeWorkspaceIdProvider se
deriva de la ruta, y no al revés—, y por eso leer datos con ámbito de espacio de
trabajo fuera de una ruta con prefijo no tiene de dónde sacar su id.
- Funciones constructoras de rutas en
router/routes.dart, cada una con el id del espacio de trabajo como primer argumento, no constantes de cadena - Cuatro rutas previas al contexto no llevan prefijo:
/splash,/onboarding,/signed-outy/workspaces(el selector) ShellRouteenvuelve el shell de la aplicación; esas cuatro superficies se representan a pantalla completa fuera de él- Guardia de autenticación: una configuración incompleta va a
/onboarding; una configuración completada cuya credencial de host de código haya desaparecido va a/signed-out. «Completa» significa que hay al menos un host de código conectado para el usuario que inició sesión y que existe al menos un espacio de trabajo —no GitHub en concreto—, y el CLIghno es un método de autenticación - Puerta de onboarding en
features/auth/providers/
Redes (lado del servidor)
Sección titulada «Redes (lado del servidor)»Todo el HTTP externo vive en el servidor. Los clientes nunca se conectan
directamente a GitHub, Linear o Google: llaman a operaciones RPC del servidor, e
incluso los medios remotos se obtienen a través del endpoint /proxy/media del
servidor en lugar de desde el origen remoto.
- Clientes dio en
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientyGoogleCalendarApiClient - Inyección del token de autenticación mediante interceptores, incluido un
interceptor OAuth de Google por cuenta en
cc_server_coreque renueva los tokens al recibir un 401 - Todos los errores se mapean a través de
packages/cc_infra/lib/src/network/error_mapper.dart→ subclases tipadas deAppExceptiondefinidas encc_domain
Seguridad
Sección titulada «Seguridad»El servidor guarda las credenciales que importan: los tokens de GitHub y de tickets para los lanzamientos de agentes, las claves de proveedores de LLM y los tokens OAuth viven todos bajo el directorio de datos del servidor o en su entorno. Lo que el llavero del cliente aún guarda es su propia credencial de dispositivo y la clave de emparejamiento de un servidor remoto.
- Secretos en
flutter_secure_storage(llavero de macOS, administrador de credenciales de Windows, libsecret de Linux): la credencial de dispositivo del cliente y las claves de emparejamiento remoto, no los tokens de proveedores shared_preferencessolo para preferencias no sensibles (tema, tipografía, diseño)- Los secretos de proveedores sobrantes de instalaciones anteriores se entregan
al servidor una sola vez mediante
features/auth/providers/credential_migration.darty luego se eliminan del llavero del cliente
Cumplimiento de la arquitectura
Sección titulada «Cumplimiento de la arquitectura»Las restricciones de arquitectura se validan con
test/core/architecture_constraints_test.dart, que falla si se violan las reglas
de dependencia.
Conceptos relacionados
Sección titulada «Conceptos relacionados»- Espacios de trabajo y aislamiento: cómo se aplica el aislamiento
- Ciclo de vida del despacho de un agente: el flujo de despacho
- Eventos de dominio: comunicación entre funcionalidades
- Implementación y clientes: el modelo de servidor de cliente ligero y la matriz de clientes