Ir al contenido

Despliegue y clientes

Control Center no es un único programa. Es una arquitectura de cliente ligero: un servidor headless que posee 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 perezosa — 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 carece de estado: cada llamada con ámbito 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 ámbito de espacio de trabajo; si lo tiene y no ha llegado ningún workspace_id, la llamada se rechaza.
  2. El id indicado debe ser un espacio de trabajo registrado, y se comprueba 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 petición. 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 administrador.
  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 el forge.

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 única fuente de verdad. Un escritorio y un teléfono, o un portátil y un escritorio, deben ver el mismo estado de la flota. Si cada uno tuviera su propia base de datos, se desviarían. Un único 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á poseer 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 teléfono en el tren o desde un navegador en cualquier sitio — es más útil que una interfaz gráfica que debes dejar abierta.

La contrapartida es que el servidor pasa a ser 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 arranque 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 posee 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 sitio 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, de modo 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 sitios 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 el mismo portátil — ws://localhost:9030, que no necesita certificado porque los navegadores consideran fiable 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 o bien su propio certificado, o 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 añadir 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 petición 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 relay WebSocket negociado y sellado de extremo a extremo, y habla un subconjunto de la superficie de herramientas predominantemente de lectura: leer tickets, mensajes y las noticias; enviar una respuesta o actualizar un ticket.

Un teléfono es un principal con menos privilegios que un agente local. Una política de herramientas de denegación por defecto, 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 comprobaciones 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 con lease 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 con lease sigue siendo solo la fontanería del lease. 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 la ruta de streaming se ejercite 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 entorno aislado tiene el host (ninguno en Windows). Un worker dedicado además anuncia siempre alwaysOn y acceptsParallel, que es contra lo que coloca el planificador pin → prefer → spill. No hay sonda de GPU ni de ML.

Un escritorio en solitario 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 con lease.

Asunto Servidor (cc_server) Ejecutor de flota (cc_worker) Cliente de escritorio Cliente web / teléfono
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 cede con lease) ejecuta los trabajos con lease — —
Representación de la interfaz — — completa completa (web) / mayormente de lectura (teléfono)
Captura de reuniones (micrófono + audio de la reunión) — — captura nativa solo web, mediante el 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 entorno aislado 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 entorno aislado. Anuncia una capacidad sandbox para que el servidor pueda colocar trabajos por plataforma, pero nada en el worker aplica ninguna: un trabajo con lease se lanza directamente, acotado por el entorno del lease y el worktree que se le ha dado. Ceder trabajo hacia fuera, por tanto, saca la ejecución del entorno aislado incluso en un host que tenga uno. Consulta Entorno aislado 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 armario 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, teléfono) usan una clave emitida por el comando pair del servidor y guardada en el llavero del sistema operativo (o, en el teléfono, localmente).

La revocación es inmediata, 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 relay. 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 teléfono 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 teléfono y el modelo de seguridad