Saltar para o conteúdo

Implementação e clientes

O Control Center não é um único programa. É uma arquitetura de cliente leve: um servidor headless que é dono dos seus dados e um conjunto de clientes que apresentam uma interface sobre uma única ligação RPC a esse servidor. Compreender esta separação explica porque é que algumas coisas correm onde correm e o que cada cliente consegue e não consegue alcançar.

Um processo chamado cc_server detém as bases de dados — uma pequena global.db para o estado de todo o servidor mais um ficheiro SQLite por espaço de trabalho, aberto de forma preguiçosa — e executa o trabalho de longa duração: motores de pipelines, reconciliadores, a superfície de ferramentas MCP e o ouvinte de orquestração. Tudo o que precisa de sobreviver a uma janela, ou que dois clientes devem ver de forma idêntica, vive aí.

Nenhum cliente abre um ficheiro de base de dados por si. Em vez disso, cada cliente detém um RemoteRpcClient e emite operações de repo e subscrições através dele. O servidor autentica a ligação com uma chave pré-partilhada de dispositivo emparelhado — e depois deixa de se preocupar com a ligação. Uma sessão não está vinculada a um espaço de trabalho. O distribuidor é sem estado: cada chamada com âmbito de espaço de trabalho transporta o seu próprio workspace_id nos argumentos, e uma chamada que o omita é recusada.

Isso é deliberado e é o que permite que dois clientes no mesmo servidor fiquem em espaços de trabalho diferentes sem um “espaço de trabalho atual” detido pelo servidor sobre o qual possam discordar. O isolamento é imposto por chamada, em quatro passos:

  1. A operação declara se tem âmbito de espaço de trabalho; se tiver e não chegar nenhum workspace_id, a chamada é rejeitada.
  2. O id indicado tem de ser um espaço de trabalho registado, verificado antes de algo abrir uma base de dados — caso contrário, um id desconhecido materializaria um workspace.db fantasma vazio em cada pedido. Uma falha é um não-encontrado.
  3. O WorkspaceRole de quem chama tem de cumprir o mínimo da operação, derivado do seu tipo: ler exige convidado, alterar exige membro, destruir exige administrador.
  4. Uma operação que expõe conteúdo de repo exige ainda uma concessão por repo ao nível que declara — none / read / review / write — para que a pertença a um espaço de trabalho nunca ultrapasse silenciosamente os privilégios da forge.

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

Três pressões empurraram no sentido de um servidor:

  1. Uma única fonte de verdade. Um computador de secretária e um telemóvel, ou um portátil e um computador de secretária, devem ver o mesmo estado da frota. Se cada um tivesse a sua própria base de dados, iriam divergir. Um único servidor significa que todos os clientes leem os mesmos dados em tempo real.
  2. A web não se pode servir sozinha. Um navegador não consegue lançar um subprocesso nem abrir um ficheiro local, por isso uma build web nunca pode ser dona da base de dados nem executar um agente. Tem de falar com um servidor que o faça. Em vez de construir um segundo caminho de dados exclusivamente web, o computador foi trazido para o mesmo caminho.
  3. Operação headless. Um servidor que pode executar numa máquina pequena — acessível a partir de um telemóvel no comboio ou de um navegador em qualquer lugar — é mais útil do que uma GUI que tem de manter aberta.

O compromisso é que o servidor passa a ser uma dependência: se não estiver a correr, nada funciona. A resposta predefinida do computador para isso é lançar um por conta própria.

Cinco clientes chegam ao servidor, cada um com uma forma e um perfil de confiança diferentes.

A aplicação Flutter para computador (macOS, Windows, Linux) é o cliente completo. No primeiro arranque, pergunta como deve ser executada e depois memoriza a escolha:

  • Local: lançar e supervisionar um cc_server nesta máquina — a configuração predefinida, autónoma e de um único utilizador. O computador lança o servidor, que é dono da base de dados sob a raiz de suporte da aplicação e fala com ele através de loopback.
  • Remoto: ligar a um cc_server a correr noutro local através de um WebSocket seguro. Os dados vivem nesse servidor; este computador é puramente um renderizador.

Nos dois casos, o cliente RPC resultante substitui o fornecedor de dados da aplicação, pelo que toda a interface — cada ecrã, cada funcionalidade — lê e escreve através do servidor. O computador nunca toca diretamente no ficheiro de base de dados.

A build web é a mesma aplicação Flutter compilada para correr num navegador, alojada em app.usectrl.dev, por isso não há nada para instalar. Um navegador não consegue lançar um subprocesso, pelo que o cliente web é sempre remoto: liga-se a um cc_server por wss:// e apresenta toda a interface de computador. Uma vez ligado, mostra os mesmos ecrãs sobre a mesma frota, porque é o mesmo código sobre o mesmo RPC. As lacunas são o punhado de locais que precisam de uma capacidade nativa que um navegador não tem — a camada de qualidade da captura de reuniões abaixo é a principal e as notificações do sistema operativo são outra.

É por isso que “sem transferência” e “sem servidor” são afirmações diferentes. O cliente web elimina a instalação; não elimina o servidor. Aquilo a que se liga pode ser um cc_server no mesmo portátil — ws://localhost:9030, que não precisa de certificado porque os navegadores consideram o loopback fiável — ou um noutra máquina, que tem de estar acessível por wss:// com um certificado em que o navegador confie. O cc_server recusa associar-se a um endereço que não seja de loopback sem TLS, por isso um servidor remoto implica ou o seu próprio certificado, ou um proxy que termine TLS, ou um túnel ou VPN que forneça um.

O próprio pacote está alojado separadamente do servidor com que fala. Um cc_server autónomo serve o ponto final RPC, a superfície MCP e os seus proxies, mas não um pacote web — o caminho de ficheiros estáticos existe no servidor, mas não está configurada nenhuma raiz web no binário autónomo, por isso não tem nada para servir em /. A imagem de contentor cc-webapp publicada é o que aloja o pacote e scripts/build_web.sh é o que o compila. Um servidor só aceita origens de navegador na sua lista --allowed-origins, que tem como predefinição https://app.usectrl.dev, por isso alojar o pacote por conta própria implica adicionar a sua origem.

Os conteúdos multimédia (avatares, imagens do feed, capturas de PR) são encaminhados pelo ponto final de proxy do servidor em vez de contactarem diretamente os servidores a montante, por isso um navegador nunca vai buscar uma origem arbitrária. O cliente implementado aplica uma Content-Security Policy por pedido a partir de um cookie não sensível, para que só a origem do servidor ligado seja permitida.

A aplicação Remote é um cliente separado e mais leve — uma PWA Flutter para web em remote.usectrl.dev. Deliberadamente não é a aplicação completa. Emparelha através de um relay WebSocket intermediado e selado ponto a ponto, e fala uma fatia predominantemente de leitura da superfície de ferramentas: ler tickets, mensagens e as notícias; enviar uma resposta ou atualizar um ticket.

Um telemóvel é um principal com menos privilégios do que um agente local. Uma política de ferramentas de negação por predefinição, uma chave pré-partilhada 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 função e de concessão por repo por chamada acima referidas e a limitação de taxa impedem que um emparelhamento aprovado (ou divulgado) se torne controlo remoto total. Ver Controlo remoto e telemóvel para o modelo de segurança completo.

Ferramentas externas — editores, outros agentes, CLIs — que falam o Model Context Protocol ligam-se à superfície MCP do servidor e consomem o registo de ferramentas tipadas. Este é o caminho de integração programática; não é uma GUI. Ver Usar o servidor MCP e Ferramentas MCP.

Um cc_worker é um binário headless de Dart puro que emparelha com um cc_server, declara as capacidades do seu anfitrião e recolhe trabalhos em lease para executar. Transmite eventos de processo de volta pela mesma RPC e não detém nenhum estado duradouro: sem base de dados, sem autenticação, sem aprovações, sem orçamentos — isso nunca sai do cc_server. Um worker é um braço, não um segundo cérebro. Um servidor autoritativo, N braços burros; não há consenso nem tráfego entre workers.

A execução em lease ainda é canalização de leases. 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 testam atualmente com git --version / rev-parse HEAD para que o caminho de streaming seja exercitado sem fingir que esses trabalhos correram.

O que anuncia é o sistema operativo, a arquitetura de CPU, o número de núcleos, a RAM, se um Flutter SDK está acessível e que backends de sandbox o anfitrião tem (nenhum no Windows). Um worker dedicado anuncia também sempre alwaysOn e acceptsParallel, que são os critérios em função dos quais o escalonador pin → prefer → spill faz a colocação. Não há qualquer sondagem de GPU nem de ML.

Um computador que funcione sozinho não dá por nada disto, porque o despacho normal não passa pela frota de todo. Existe um worker implícito no próprio processo registado para o anfitrião do servidor, mas é apenas uma costura de registo sem runners ligados a ela — um trabalho colocado no worker local falha em vez de ser executado. Hoje, só um cc_worker real executa trabalhos em lease.

Aspeto Servidor (cc_server) Executor da frota (cc_worker) Cliente de computador Cliente web / telemóvel
Bases de dados (Drift/SQLite) é dono delas — — —
Autenticação, aprovações, orçamentos, identidade é dono disso — — —
Pipelines, reconciliadores, ouvinte de orquestração executa — — —
Superfície de ferramentas MCP serve-a — — —
Execução de agentes executa-a (ou passa-a por lease) executa trabalhos em lease — —
Apresentação da interface — — completa completa (web) / sobretudo de leitura (telemóvel)
Captura de reuniões (microfone + áudio da reunião) — — captura nativa só na web, através do áudio da partilha de ecrã
OAuth do calendário (Google) guarda os tokens — — —

Os dois níveis não aplicam a mesma sandbox. O cc_server envolve as execuções que ele próprio realiza na sandbox do sistema operativo do anfitrião, 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 nenhuma. Anuncia uma capacidade sandbox para que o servidor possa colocar trabalhos por plataforma, mas nada no worker aplica uma: um trabalho em lease é lançado diretamente, limitado pelo ambiente do lease e pela worktree que lhe foi atribuída. Entregar trabalho por lease desloca, por isso, a execução para fora da sandbox, mesmo num anfitrião que tenha uma. Ver Sandbox e segurança para saber o que limita uma execução em cada caso.

A captura de reuniões acontece num cliente porque precisa de um microfone e do áudio da reunião — um servidor guardado num armário não consegue gravar a sua chamada. A gravação é depois resumida pelo pipeline do servidor e as notas são guardadas onde todos os clientes as possam ler.

O cliente web também grava, algo que a forma da tabela pode obscurecer: capta o microfone mais uma faixa de áudio de partilha de ecrã no navegador e transmite PCM a 16 kHz para o servidor. Exige essa faixa de áudio, por isso falha explicitamente onde o navegador não a forneça. O que é genuinamente exclusivo do computador é a camada de qualidade em torno da captura — cancelamento de eco ao nível do sinal através do processador nativo, o medidor de nível de entrada e o aviso de microfone inativo, voltar a executar um resumo, cancelar o processamento e a ação de gravar e vincular do calendário.

Cada cliente prova quem é com uma chave pré-partilhada fornecida uma única vez:

  • O computador em modo local gera uma chave nova em cada arranque e entrega-a ao servidor lançado através do ambiente, para que nada secreto seja persistido no computador.
  • Os clientes remotos (computador remoto, web, telemóvel) usam uma chave emitida pelo comando pair do servidor e guardada no porta-chaves do sistema operativo (ou, no telemóvel, localmente).

A revogação é imediata, não preguiçosa. O servidor vigia a sua tabela de dispositivos emparelhados e derruba qualquer sessão aberta cujo dispositivo tenha saído do conjunto ativo, em segundos — tanto no caminho direto por WebSocket como no caminho do relay. Não tem de esperar que o dispositivo se volte a ligar para o cortar.

A chave nunca viaja através de um URL que um alojamento estático possa ver. Quando chega por link — no telemóvel ou num navegador — vai no fragmento do URL, a parte depois de #, que o navegador nunca envia ao servidor que aloja a página. Essa é a invariante que vale a pena recordar, e aplica-se a ambos. O cliente web tem dois outros pontos de entrada: o formulário de ligação, onde cola diretamente um servidor e uma chave, e o código de convite, que troca por uma credencial emitida só para esse navegador.