Ir al contenido

Implementación y clientes

Control Center no es un único programa. Es una arquitectura de cliente ligero: un servidor headless que es dueño de tus datos y un conjunto de clientes que dibujan una interfaz sobre una única conexión RPC con él. Entender esta separación explica por qué algunas cosas se ejecutan donde se ejecutan y a qué puede llegar cada cliente y a qué no.

Un proceso llamado cc_server guarda las bases de datos — una pequeña global.db para el estado global del servidor y un archivo SQLite por espacio de trabajo, que se abre de forma diferida — y ejecuta el trabajo de larga duración: motores de pipelines, reconciliadores, la superficie de herramientas MCP y el listener de orquestación. Todo lo que debe sobrevivir al cierre de una ventana, o que dos clientes deben ver de forma idéntica, vive ahí.

Ningún cliente abre por su cuenta un archivo de base de datos. En su lugar, cada cliente tiene un único RemoteRpcClient y lanza por él las operaciones de repositorio y las suscripciones. El servidor autentica la conexión contra una clave precompartida de dispositivo emparejado — y a partir de ahí deja de preocuparse por la conexión. Una sesión no está vinculada a un espacio de trabajo. El despachador no tiene estado: cada llamada con alcance de espacio de trabajo lleva su propio workspace_id en sus argumentos, y una llamada que lo omita se rechaza.

Eso es deliberado, y es lo que permite que dos clientes de un mismo servidor estén en espacios de trabajo distintos sin que el servidor mantenga un «espacio de trabajo actual» sobre el que puedan discrepar. El aislamiento se aplica por llamada, en cuatro pasos:

  1. La operación declara si tiene alcance de espacio de trabajo; si lo tiene y no llegó ningún workspace_id, la llamada se rechaza.
  2. El id indicado debe ser un espacio de trabajo registrado, y se verifica antes de que nada abra una base de datos; de lo contrario, un id desconocido materializaría un workspace.db fantasma y vacío en cada solicitud. Un fallo es un no encontrado.
  3. El WorkspaceRole de quien llama debe alcanzar el mínimo de la operación, derivado de su tipo: leer necesita invitado, modificar necesita miembro, destruir necesita admin.
  4. Una operación que expone contenido de repositorio exige además un permiso por repositorio al nivel que declara — none / read / review / write — para que la pertenencia al espacio de trabajo nunca otorgue en silencio más privilegios que la forja.

La frase que importa: tener una clave de emparejamiento no es la frontera de acceso; la pertenencia lo es.

Tres presiones empujaron hacia un servidor:

  1. Una sola fuente de verdad. Un escritorio y un celular, o una laptop y una computadora de escritorio, deben ver el mismo estado de la flota. Si cada uno tuviera su propia base de datos, se desviarían. Un solo servidor hace que todos los clientes lean los mismos datos en vivo.
  2. La web no puede servirse a sí misma. Un navegador no puede lanzar un subproceso ni abrir un archivo local, así que una compilación web nunca podrá ser dueña de la base de datos ni ejecutar un agente. Tiene que hablar con un servidor que sí pueda. En lugar de construir una segunda ruta de datos solo para la web, se llevó el escritorio a la misma ruta.
  3. Funcionamiento headless. Un servidor que puedes ejecutar en una máquina pequeña — accesible desde un celular en el tren o desde un navegador en cualquier lugar — es más útil que una interfaz gráfica que debes dejar abierta.

La contrapartida es que el servidor ahora es una dependencia: si no está en marcha, nada funciona. La respuesta predeterminada del escritorio es lanzar uno él mismo.

Cinco clientes llegan al servidor, cada uno con una forma y un perfil de confianza distintos.

La aplicación de escritorio de Flutter (macOS, Windows, Linux) es el cliente completo. En el primer inicio pregunta cómo debe ejecutarse y luego recuerda la elección:

  • Local: lanza y supervisa un cc_server en esta máquina — la configuración predeterminada, autocontenida y para un solo usuario. El escritorio inicia el servidor, que es dueño de la base de datos bajo la raíz de soporte de la aplicación y habla con él por loopback.
  • Remoto: se conecta a un cc_server que se ejecuta en otro lugar mediante un WebSocket seguro. Los datos viven en ese servidor; este escritorio es solo un renderizador.

En ambos casos, el cliente RPC resultante sustituye al proveedor de datos de la aplicación, así que toda la interfaz — cada pantalla, cada función — lee y escribe a través del servidor. El escritorio nunca toca directamente el archivo de la base de datos.

La compilación web es la misma aplicación Flutter compilada para ejecutarse en un navegador, alojada en app.usectrl.dev, así que no hay nada que instalar. Un navegador no puede lanzar un subproceso, por lo que el cliente web es siempre remoto: marca un cc_server por wss:// y dibuja la interfaz de escritorio completa. Una vez conectado, muestra las mismas pantallas sobre la misma flota, porque es el mismo código sobre el mismo RPC. Lo que falta son el puñado de lugares que necesitan una capacidad nativa que un navegador no tiene: la capa de calidad de la captura de reuniones que aparece más abajo es la principal, y las notificaciones del sistema operativo son otra.

Por eso «sin descarga» y «sin servidor» son afirmaciones distintas. El cliente web elimina la instalación; no elimina el servidor. A lo que marca puede ser un cc_server en la misma laptop — ws://localhost:9030, que no necesita certificado porque los navegadores consideran confiable el loopback — o uno en otra máquina, que debe ser accesible por wss:// con un certificado en el que el navegador confíe. cc_server se niega a enlazar una dirección que no sea de loopback sin TLS, así que un servidor remoto implica su propio certificado, un proxy que termine TLS, o un túnel o VPN que lo aporte.

El bundle en sí se aloja aparte del servidor con el que habla. Un cc_server independiente sirve el endpoint RPC, la superficie MCP y sus proxies, pero no un bundle web: la ruta de archivos estáticos existe en el servidor, pero en el binario independiente no hay ninguna raíz web configurada, así que no tiene nada que servir en /. La imagen de contenedor publicada cc-webapp es la que aloja el bundle, y scripts/build_web.sh es lo que lo compila. Un servidor solo acepta orígenes de navegador en su lista --allowed-origins, que de forma predeterminada es https://app.usectrl.dev, así que alojar el bundle por tu cuenta implica agregar su origen.

Los medios (avatares, imágenes del feed, capturas de PR) pasan por el endpoint de proxy del servidor en lugar de llamar directamente a los hosts externos, de modo que un navegador nunca descarga un origen arbitrario. El cliente desplegado fija una Content-Security Policy por solicitud a partir de una cookie no sensible, de modo que solo se permite el origen del servidor conectado.

La aplicación Remote es un cliente aparte y más ligero: una PWA web de Flutter en remote.usectrl.dev. Deliberadamente no es la aplicación completa. Se empareja a través de un relevo WebSocket negociado y sellado de extremo a extremo, y habla un subconjunto de la superficie de herramientas mayormente de lectura: leer tickets, mensajes y el feed de noticias; enviar una respuesta o actualizar un ticket.

Un celular es un principal con menos privilegios que un agente local. Una política de herramientas de denegación predeterminada, una clave precompartida que sella cada trama, una capacidad de sesión que deja fuera de un cliente complementario las operaciones privilegiadas (el emparejamiento, por ejemplo), las verificaciones de rol y de permiso por repositorio de más arriba y la limitación de tasa impiden que un emparejamiento aprobado (o filtrado) se convierta en un control remoto total. Consulta Control remoto y móvil para ver el modelo de seguridad completo.

Las herramientas externas — editores, otros agentes, CLIs — que hablan el Model Context Protocol se conectan a la superficie MCP del servidor y consumen el registro tipado de herramientas. Es la vía de integración programática; no es una interfaz gráfica. Consulta Usar el servidor MCP y la Referencia de herramientas MCP.

Un cc_worker es un binario headless de Dart puro que se empareja con un cc_server, declara las capacidades de su host y recoge trabajos en arrendamiento para ejecutarlos. Devuelve el flujo de eventos de proceso por el mismo RPC y no guarda ningún estado duradero: ni base de datos, ni autenticación, ni aprobaciones, ni presupuestos — nada de eso sale nunca de cc_server. Un worker es una extremidad, no un segundo cerebro. Un servidor autoritativo, N extremidades tontas; no hay consenso ni tráfico entre workers.

La ejecución en arrendamiento sigue siendo solo la fontanería del arrendamiento. agentRun es el único tipo que ejecuta un comando real (CC_JOB_COMMAND, o un prompt devuelto tal cual si no se proporcionó ninguno). pipelineStep, codeIndex, goldenRender, benchmark y evalBatch actualmente sondean con git --version / rev-parse HEAD para que se ejercite la ruta de streaming sin fingir que esos trabajos se ejecutaron.

Lo que anuncia es el sistema operativo, la arquitectura de CPU, el número de núcleos, la RAM, si hay un SDK de Flutter accesible y qué backends de sandbox tiene el host (ninguno en Windows). Un worker dedicado además siempre anuncia alwaysOn y acceptsParallel, que es contra lo que coloca el planificador pin → prefer → spill. No hay sonda de GPU ni de ML.

Una computadora de escritorio por sí sola no nota nada de esto, porque el despacho ordinario no pasa por la flota en absoluto. Sí hay un worker implícito en proceso registrado para el host del servidor, pero es una costura de registro sin ningún runner conectado: un trabajo que de hecho se coloque en el worker local falla en lugar de ejecutarse. Hoy en día, solo un cc_worker real ejecuta trabajos en arrendamiento.

Aspecto Servidor (cc_server) Ejecutor de flota (cc_worker) Cliente de escritorio Cliente web / celular
Bases de datos (Drift/SQLite) las posee — — —
Autenticación, aprobaciones, presupuestos, identidad las posee — — —
Pipelines, reconciliadores, listener de orquestación los ejecuta — — —
Superficie de herramientas MCP la sirve — — —
Ejecución de agentes la ejecuta (o la arrienda) ejecuta los trabajos en arrendamiento — —
Representación de la interfaz — — completa completa (web) / mayormente de lectura (celular)
Captura de reuniones (micrófono + audio de la reunión) — — captura nativa solo web, mediante audio de pantalla compartida
OAuth de calendario (Google) guarda los tokens — — —

Las dos capas no aplican el mismo aislamiento. cc_server envuelve las ejecuciones que realiza él mismo en el sandbox del sistema operativo del host cuando existe un backend — Seatbelt en macOS, bubblewrap en Linux y WSL2 — y recurre a las demás fronteras en Windows o cuando no hay bwrap y socat. cc_worker no aplica ningún sandbox. Anuncia una capacidad sandbox para que el servidor pueda colocar trabajos por plataforma, pero nada en el worker aplica ninguna: un trabajo en arrendamiento se lanza directamente, acotado por el entorno del arrendamiento y el worktree que se le dio. Ceder trabajo en arrendamiento, por lo tanto, saca la ejecución fuera del sandbox incluso en un host que tenga uno. Consulta Sandbox y seguridad para saber qué acota una ejecución en cada caso.

La captura de reuniones ocurre en un cliente porque necesita un micrófono y el audio de la reunión: un servidor metido en un clóset no puede grabar tu llamada. Después, el pipeline del servidor resume la grabación y las notas se guardan donde todos los clientes pueden leerlas.

El cliente web también graba, algo que la forma de la tabla puede ocultar: captura el micrófono y una pista de audio de pantalla compartida en el navegador, y envía PCM de 16 kHz al servidor. Requiere esa pista de audio, así que falla de forma explícita donde el navegador no la proporcione. Lo que es genuinamente exclusivo del escritorio es la capa de calidad en torno a la captura: cancelación de eco a nivel de señal mediante el procesador nativo, el medidor de nivel de entrada y el aviso de micrófono sin señal, volver a ejecutar un resumen, cancelar el procesamiento y la acción de grabar y vincular del calendario.

Cada cliente demuestra quién es con una clave precompartida aprovisionada una sola vez:

  • El escritorio en modo local genera una clave nueva en cada arranque y se la pasa al servidor que lanza mediante el entorno, así que en el escritorio no se persiste nada secreto.
  • Los clientes remotos (escritorio remoto, web, celular) usan una clave emitida por el comando pair del servidor y guardada en el llavero del sistema operativo (o, en el celular, localmente).

La revocación es en vivo, no diferida. El servidor vigila su tabla de dispositivos emparejados y corta cualquier sesión abierta cuyo dispositivo haya salido del conjunto activo, en cuestión de segundos, tanto en la ruta directa de WebSocket como en la del relevo. No tienes que esperar a que el dispositivo se vuelva a conectar para cortarlo.

La clave nunca viaja por una URL que un host estático pueda ver. Cuando llega por un enlace — en el celular o en un navegador — va en el fragmento de la URL, la parte posterior a #, que el navegador nunca envía al servidor que aloja la página. Esa es la invariante que conviene recordar, y se aplica a ambos. El cliente web tiene otros dos puntos de entrada: el formulario de conexión, donde pegas un servidor y una clave directamente, y un código de invitación, que canjea por una credencial emitida solo para ese navegador.

  • Espacios de trabajo y aislamiento: por qué cada llamada RPC lleva su propio id de espacio de trabajo y cómo la división de la base de datos hace estructural el aislamiento
  • Arquitectura: cómo está estructurado el código en torno al servidor y sus clientes
  • Control remoto y móvil: el emparejamiento del celular y el modelo de seguridad