Pular para o conteúdo

Implantação e clientes

O Control Center não é um único programa. É uma arquitetura de cliente leve: um servidor headless que detém seus dados e um conjunto de clientes que renderizam uma interface por meio de uma única conexão RPC com ele. Entender essa divisão explica por que algumas coisas rodam onde rodam e o que cada cliente consegue ou não alcançar.

Um processo chamado cc_server detém os bancos de dados — um pequeno global.db para o estado de todo o servidor, mais um arquivo SQLite por espaço de trabalho, abertos sob demanda — e executa o trabalho de longa duração: os mecanismos de pipeline, os reconciliadores, a superfície de ferramentas MCP e o ouvinte de orquestração. Tudo o que precisa sobreviver ao fechamento de uma janela, ou que dois clientes devem ver de forma idêntica, fica ali.

Nenhum cliente abre um arquivo de banco de dados por conta própria. Em vez disso, cada cliente mantém um único RemoteRpcClient e emite, por meio dele, as operações de repositório e as assinaturas. O servidor autentica a conexão com base em uma chave pré-compartilhada de dispositivo emparelhado — e depois deixa de se importar com a conexão. Uma sessão não está vinculada a um espaço de trabalho. O despachante não mantém estado: toda chamada com escopo de espaço de trabalho carrega o próprio workspace_id em seus argumentos, e uma chamada que o omita é recusada.

Isso é intencional, e é o que permite que dois clientes em um mesmo servidor fiquem em espaços de trabalho diferentes sem um “espaço de trabalho atual” mantido pelo servidor sobre o qual possam discordar. Em vez disso, o isolamento é aplicado por chamada, em quatro etapas:

  1. A operação declara se tem escopo de espaço de trabalho; se tiver e nenhum workspace_id chegar, a chamada é rejeitada.
  2. O id informado precisa ser um espaço de trabalho registrado, verificado antes de qualquer abertura de banco de dados — caso contrário, um id desconhecido materializaria um workspace.db fantasma vazio a cada requisição. Uma ausência de correspondência resulta em não encontrado.
  3. O WorkspaceRole de quem chama precisa atender ao mínimo exigido pela operação, derivado do seu tipo: leitura exige guest, mutação exige member e destruição exige admin.
  4. Uma operação que expõe conteúdo de repositório exige ainda uma autorização por repositório no nível que ela declara — none / read / review / write — para que a participação em um espaço de trabalho nunca ultrapasse silenciosamente os privilégios do host de código.

A frase que importa: ter uma chave de emparelhamento não é a fronteira de acesso; a participação é.

Três pressões levaram a um servidor:

  1. Uma única fonte de verdade. Um desktop e um celular, ou um notebook e um desktop, devem ver o mesmo estado da frota. Se cada um tivesse o próprio banco de dados, eles divergiriam. Com um único servidor, todo cliente lê os mesmos dados ao vivo.
  2. A web não consegue funcionar sozinha. Um navegador não pode criar um subprocesso nem abrir um arquivo local, então uma build web nunca pode deter o banco de dados nem executar um agente. Ela precisa falar com um servidor que faça isso. Em vez de criar um segundo caminho de dados exclusivo da web, o desktop foi trazido para o mesmo caminho.
  3. Operação headless. Um servidor que você pode executar em uma máquina pequena — acessível de um celular no trem ou de um navegador em qualquer lugar — é mais útil do que uma interface gráfica que você precisa deixar aberta.

O custo dessa escolha é que o servidor passa a ser uma dependência: se ele não estiver em execução, nada funciona. A resposta padrão do desktop para isso é iniciar um por conta própria.

Cinco clientes alcançam o servidor, cada um com formato e perfil de confiança diferentes.

O aplicativo de desktop em Flutter (macOS, Windows, Linux) é o cliente completo. Na primeira inicialização, ele pergunta como deve ser executado e depois lembra a escolha:

  • Local: iniciar e supervisionar um cc_server nesta máquina — a configuração padrão, autocontida e de usuário único. O desktop inicia o servidor, que detém o banco de dados sob a raiz de suporte do aplicativo e conversa com ele pela interface de loopback.
  • Remoto: conectar-se a um cc_server em execução em outro lugar por um WebSocket seguro. Os dados ficam nesse servidor; este desktop é puramente um renderizador.

Nos dois casos, o cliente RPC resultante substitui o provedor de dados do aplicativo, de modo que toda a interface — cada tela, cada recurso — lê e grava por meio do servidor. O desktop nunca toca diretamente no arquivo de banco de dados.

A build web é o mesmo aplicativo Flutter compilado para rodar em um navegador, hospedado em app.usectrl.dev, então não há nada para instalar. Um navegador não pode criar um subprocesso, então o cliente web é sempre remoto: ele disca para um cc_server por wss:// e renderiza a interface de desktop completa. Depois de conectado, ele mostra as mesmas telas contra a mesma frota, porque é o mesmo código sobre o mesmo RPC. As lacunas são o punhado de lugares que exigem uma capacidade nativa que um navegador não tem — a camada de qualidade da captura de reunião abaixo é a principal, e as notificações do sistema operacional são outra.

É por isso que “sem download” e “sem servidor” são afirmações diferentes. O cliente web elimina a instalação; ele não elimina o servidor. Aquilo a que ele se conecta pode ser um cc_server no mesmo notebook — ws://localhost:9030, que dispensa certificado porque os navegadores consideram o loopback confiável — ou um em outra máquina, que precisa ser acessível por wss:// com um certificado em que o navegador confie. O cc_server se recusa a vincular um endereço que não seja de loopback sem TLS, então um servidor remoto significa ou um certificado próprio, ou um proxy que termina o TLS, ou um túnel ou VPN que forneça um.

O bundle em si é hospedado separadamente do servidor com que conversa. Um cc_server autônomo serve o endpoint RPC, a superfície MCP e seus proxies, mas não um bundle web — o caminho de arquivos estáticos existe no servidor, mas nenhuma raiz web está configurada no binário autônomo, então ele não tem nada a servir em /. A imagem de contêiner cc-webapp publicada é o que hospeda o bundle, e scripts/build_web.sh é o que o compila. Um servidor só aceita origens de navegador que estejam em sua lista --allowed-origins, cujo padrão é https://app.usectrl.dev, então hospedar o bundle por conta própria significa adicionar a origem dele.

Mídias (avatares, imagens do feed, capturas de tela de PR) passam pelo endpoint de proxy do servidor em vez de acessar diretamente os hosts upstream, então um navegador nunca busca uma origem arbitrária. O cliente implantado aplica uma Content-Security Policy por requisição a partir de um cookie não sensível, de modo que apenas a origem do servidor conectado seja permitida.

O aplicativo Remote é um cliente separado e mais leve — uma PWA web em Flutter em remote.usectrl.dev. Ele é intencionalmente não o aplicativo completo. Ele se emparelha por um relay WebSocket intermediado e selado de ponta a ponta e fala uma fatia predominantemente de leitura da superfície de ferramentas: ler tickets, mensagens e o feed de notícias; enviar uma resposta ou atualizar um ticket.

Um celular é um principal com menos privilégios do que um agente local. Uma política de ferramentas de negação por padrão, uma chave pré-compartilhada que sela cada frame, uma capacidade de sessão que exclui operações privilegiadas (o emparelhamento, por exemplo) de um cliente companheiro, as verificações de papel e de autorização por repositório a cada chamada descritas acima e a limitação de taxa impedem que um emparelhamento aprovado (ou vazado) se torne controle remoto total. Consulte Controle remoto e mobile para ver o modelo de segurança completo.

Ferramentas externas — editores, outros agentes, CLIs — que falam o Model Context Protocol se conectam à superfície MCP do servidor e consomem o registro tipado de ferramentas. Este é o caminho de integração programática; não é uma GUI. Consulte Usar o servidor MCP e a referência de ferramentas MCP.

Um cc_worker é um binário headless em Dart puro que se emparelha com um cc_server, declara as capacidades do seu host e busca tarefas em lease para executar. Ele transmite os eventos de processo de volta pelo mesmo RPC e não detém nenhum estado durável: sem banco de dados, sem autenticação, sem aprovações, sem orçamentos — nada disso sai do cc_server. Um worker é um braço, não um segundo cérebro. Um servidor autoritativo, N braços sem inteligência; não existe consenso nem tráfego de worker para worker.

A execução em lease ainda é encanamento de lease. agentRun é o único tipo que executa um comando real (CC_JOB_COMMAND, ou um prompt ecoado, se nenhum tiver sido fornecido). pipelineStep, codeIndex, goldenRender, benchmark e evalBatch hoje fazem uma sondagem com git --version / rev-parse HEAD para que o caminho de streaming seja exercitado sem fingir que aquelas tarefas foram executadas.

O que ele anuncia é o sistema operacional, a arquitetura de CPU, o número de núcleos, a RAM, se um SDK do Flutter está acessível e quais backends de sandbox o host possui (nenhum no Windows). Um worker dedicado também sempre anuncia alwaysOn e acceptsParallel, que é o critério que o agendador pin → prefer → spill usa para posicionar as tarefas. Não há sondagem de GPU nem de ML.

Um desktop sozinho não percebe nada disso, porque o despacho comum não passa pela frota de jeito nenhum. Há, sim, um worker implícito no próprio processo, registrado para o host do servidor, mas é apenas uma costura de registro sem nenhum executor conectado a ela — uma tarefa de fato colocada no worker local falha em vez de ser executada. Hoje, somente um cc_worker de verdade executa tarefas em lease.

Aspecto Servidor (cc_server) Executor da frota (cc_worker) Cliente de desktop Cliente web / de celular
Bancos de dados (Drift/SQLite) detém — — —
Autenticação, aprovações, orçamentos, identidade detém — — —
Pipelines, reconciliadores, ouvinte de orquestração executa — — —
Superfície de ferramentas MCP serve — — —
Execução de agentes executa (ou repassa em lease) executa tarefas em lease — —
Renderização da interface — — completa completa (web) / predominantemente leitura (celular)
Captura de reunião (microfone + áudio da reunião) — — captura nativa somente web, via áudio de compartilhamento de tela
OAuth do calendário (Google) armazena os tokens — — —

Os dois níveis não aplicam sandbox da mesma forma. O cc_server envolve as execuções que ele mesmo realiza no sandbox do sistema operacional do host onde existe um backend — Seatbelt no macOS, bubblewrap no Linux e no WSL2 — e recorre às outras fronteiras no Windows ou sem bwrap e socat. O cc_worker não aplica sandbox nenhum. Ele anuncia uma capacidade sandbox para que o servidor possa posicionar tarefas por plataforma, mas nada no worker aplica uma: uma tarefa em lease é iniciada diretamente, limitada pelo ambiente do lease e pela worktree que recebeu. Repassar trabalho em lease, portanto, move a execução para fora do sandbox mesmo em um host que tenha um. Consulte Sandbox e segurança para saber o que limita uma execução em cada caso.

A captura de reunião acontece em um cliente porque precisa de um microfone e do áudio da reunião — um servidor dentro de um armário não consegue gravar a sua chamada. A gravação é então resumida pelo pipeline do servidor, e as notas são armazenadas onde todo cliente possa lê-las.

O cliente web também grava, algo que o formato da tabela pode obscurecer: ele capta o microfone mais uma faixa de áudio de compartilhamento de tela no navegador e transmite PCM de 16 kHz para o servidor. Ele exige essa faixa de áudio, então falha explicitamente onde o navegador não a fornece. O que é genuinamente exclusivo do desktop é a camada de qualidade em torno da captura — o cancelamento de eco em nível de sinal pelo processador nativo, o medidor de nível de entrada e o aviso de microfone mudo, a nova execução de um resumo, o cancelamento do processamento e a ação de gravar e vincular do calendário.

Todo cliente prova quem é com uma chave pré-compartilhada provisionada uma única vez:

  • O desktop em modo local gera uma chave nova a cada inicialização e a entrega ao servidor iniciado por meio do ambiente, de modo que nenhum segredo é persistido no desktop.
  • Clientes remotos (desktop remoto, web, celular) usam uma chave emitida pelo comando pair do servidor e armazenada no cofre de chaves do sistema operacional (ou, no celular, localmente).

A revogação é imediata, não preguiçosa. O servidor observa sua tabela de dispositivos emparelhados e derruba, em segundos, qualquer sessão aberta cujo dispositivo tenha saído do conjunto ativo — tanto no caminho direto por WebSocket quanto no caminho do relay. Você não precisa esperar o dispositivo se reconectar para cortá-la.

A chave nunca trafega por uma URL que um host estático possa ver. Quando ela chega por link — no celular ou no navegador —, viaja no fragmento da URL, a parte depois de #, que o navegador nunca envia ao servidor que hospeda a página. Esse é o invariante que vale a pena lembrar, e ele se aplica aos dois casos. O cliente web tem dois outros pontos de entrada: o formulário de conexão, onde você cola um servidor e uma chave diretamente, e um código de convite, que ele troca por uma credencial emitida exclusivamente para aquele navegador.

  • Espaços de trabalho e isolamento: por que cada chamada RPC carrega o próprio id de espaço de trabalho e como a divisão dos bancos de dados torna o isolamento estrutural
  • Arquitetura: como a base de código está estruturada em torno do servidor e de seus clientes
  • Controle remoto e mobile: o modelo de emparelhamento e segurança do celular