Pular para o conteúdo

Executar um servidor headless

Execute o cc_server em uma máquina que fique ligada e depois conecte-se a ele pelo seu desktop, por um navegador ou pelo seu celular. O servidor é um binário nativo em Dart puro — sem engine do Flutter — então ele roda headless no macOS, Linux ou Windows. Ele detém o banco de dados, executa os agentes e faz todas as chamadas de API externas; os clientes apenas renderizam.

Cada release publica um stack auto-hospedado de quatro imagens do GHCR — o backend, os dois clientes finos estáticos e o relay de pareamento opcional — além de uma imagem cc-server-demo separada para o host de demonstração público. Uma instalação auto-hospedada não precisa de nenhum checkout do código-fonte. Fixe uma tag de versão (ghcr.io/samuelalev/<image>:v0.0.1-rc.1) ou use latest.

Imagem O que é Publica
ghcr.io/samuelalev/cc-server O backend: banco de dados, agentes, MCP, webhooks -p 9030:9030
ghcr.io/samuelalev/cc-webapp O cliente web (nginx estático) -p 8080:8080
ghcr.io/samuelalev/cc-remote O PWA complementar para celular (nginx estático) -p 8081:8080
ghcr.io/samuelalev/cc-signaling-server O relay de pareamento WebRTC (opcional) -p 8788:8788

O stack inteiro como um arquivo compose:

services:
server:
image: ghcr.io/samuelalev/cc-server:latest
ports: ["9030:9030"]
volumes: ["cc_data:/data"]
restart: unless-stopped
web:
image: ghcr.io/samuelalev/cc-webapp:latest
ports: ["8080:8080"]
restart: unless-stopped
remote:
image: ghcr.io/samuelalev/cc-remote:latest
ports: ["8081:8080"]
restart: unless-stopped
signaling:
image: ghcr.io/samuelalev/cc-signaling-server:latest
ports: ["8788:8788"]
restart: unless-stopped
volumes:
cc_data:

Coisas que vale a pena saber antes de expor qualquer parte disso além do loopback:

  • A imagem cc-server vem com CC_SERVER_INSECURE=1 (ela precisa se vincular a 0.0.0.0 para receber o tráfego da porta publicada), o que só é seguro atrás de um proxy reverso que termina TLS — Caddy, Traefik, nginx ou um Cloudflare Tunnel — em uma rede confiável. Para terminar o TLS no próprio processo, monte um certificado e defina CC_SERVER_TLS_CERT / CC_SERVER_TLS_KEY (e então remova CC_SERVER_INSECURE). Tudo pode ser sobrescrito com docker run -e … ou o bloco environment: do compose.

  • /data é o único estado. Os bancos SQLite, os segredos dos dispositivos emparelhados, os modelos baixados e o áudio das reuniões ficam todos no volume — faça backup dele e o contêiner se torna descartável.

  • Emparelhe um cliente de dentro da imagem antes da primeira conexão, o equivalente em contêiner do passo 3:

    Janela do terminal
    docker run --rm -it -v cc_data:/data \
    --entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair
  • As duas imagens de cliente são hosts nginx estáticos — nada para configurar em tempo de execução; o formulário de conexão de cada cliente recebe a URL do servidor, o id do dispositivo e a chave de pareamento. O relay de sinalização também precisa apenas da porta dele (SIGNALING_HOST / SIGNALING_PORT sobrescrevíveis); ele não tem estado, não guarda segredos e é um fallback — um celular usa loopback, LAN, tailnet ou um caminho wss:// direto quando algum está acessível, e só recorre ao relay quando nenhum está. Pule-o se os seus clientes sempre alcançam o servidor diretamente.

O restante deste guia compila o servidor a partir do código-fonte; as flags e o comportamento abordados são os mesmos alimentados pelas variáveis de ambiente da imagem.

  • O Dart SDK. Este repositório fixa o SDK dele com fvm, então prefixe todo comando com fvm.
  • Este repositório clonado.
  • O toolchain nativo de que o script de preparação precisa — Rust/cargo, além de meson, ninja, pkg-config, git e um compilador de C++. Cada script de build diz o que não conseguiu encontrar e como instalá-lo.
  • Um host alcançável: loopback para uso local, ou um endereço de LAN/público com TLS para clientes remotos.

As bibliotecas nativas são obrigatórias — não existe modo degradado. Compile-as a partir da raiz do repositório antes de qualquer outra coisa:

Janela do terminal
scripts/natives/build_natives.sh

Isso compila rift, fff, tree-sitter e as grammars dele, aec, lame, ccpty, cc_watcher, cc_inference e a biblioteca nativa de SAML em build/natives/. O hook de build do cc_server reemite para o pacote tudo o que estiver preparado ali, então pular esta etapa ou faz o build falhar de imediato ou — com a válvula de escape abaixo — produz um binário cuja verificação de inicialização se recusa a iniciar e aponta a biblioteca ausente.

Janela do terminal
cd apps/cc_server
fvm dart build cli

O pacote traz libsqlite3 e os nativos preparados junto ao binário — não é preciso SQLite do sistema nem engine do Flutter. O executável fica em ./build/cli/<os_arch>/bundle/bin/cc_server.

Etapa 3: Emparelhe um cliente antes da primeira inicialização

Seção intitulada “Etapa 3: Emparelhe um cliente antes da primeira inicialização”

Um diretório de dados novo não tem nenhum dispositivo emparelhado nem chave pré-compartilhada, então o prompt de chave de pareamento de um cliente ainda não pode ser atendido. Gere uma com o subcomando pair. Ele grava global.db e secrets.json diretamente; um servidor que já está atendendo esse diretório de dados reconhece o novo dispositivo sem reiniciar:

Janela do terminal
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030

Ele imprime três valores para colar no formulário de conexão de um cliente:

Connect a thin client (paste into its connect form):
Server ws://localhost:9030/rpc
Device id web-client
Pairing key <a freshly generated key>

Flags úteis: --device (padrão web-client; o id também define o nível do dispositivo — desktop, ios, android, qualquer outra coisa é um cliente web), --label (o padrão é o nome da plataforma mais o hostname desta máquina), --host (o host a imprimir na URL, já que o loopback não é alcançável de outra máquina) e --client-url. Executar pair de novo rotaciona a chave desse dispositivo e derruba as sessões ativas dele. O pareamento nunca cria um espaço de trabalho: o onboarding do cliente que se conecta dá nome e cria o primeiro.

Com --client-url ele também imprime um QR code escaneável pelo terminal que abre o cliente web já preenchido:

Janela do terminal
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030 \
--bind any --host 192.168.1.42 --client-url https://app.usectrl.dev

Uma credencial gerada por cc_server pair não tem expiração; uma gerada no app sempre expira após 30 dias. Revogue um dispositivo gerado pela CLI em Configurações → Você → Seus dispositivos quando terminar de usá-lo.

No loopback — o padrão e tudo o que você precisa para um cliente na mesma máquina:

Janela do terminal
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030

Para alcançá-lo de outra máquina, vincule todas as interfaces. Um bind que não seja de loopback sem TLS é recusado na inicialização (Refusing to bind non-loopback address … without TLS), então forneça um certificado:

Janela do terminal
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem \
--public-url wss://server.example:9030/rpc

Ou termine o TLS em um proxy reverso em uma rede privada confiável e opte por um bind em texto simples atrás dele:

Janela do terminal
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --insecure --public-url wss://server.example/rpc

--insecure é ignorado quando --tls-cert e --tls-key estão ambos definidos — o TLS sempre vence. O servidor anuncia o estado de texto simples como insecure: true em GET /healthz e os clientes marcam essa conexão como insegura.

O servidor roda até receber SIGINT/SIGTERM e então encerra de forma limpa.

Aponte um cliente para a URL do servidor e a chave de pareamento da etapa 3:

  • App de desktop: na tela de primeira execução (Como o Control Center deve ser executado?) preencha os campos de conexão em vez de Executar neste aplicativo, ou adicione o servidor depois em Configurações → Servidor → Conexão e status. Consulte Conectar a um servidor remoto.
  • Cliente web: abra app.usectrl.dev (ou a sua própria cópia do build web) e informe a URL do servidor, o id do dispositivo e a chave de pareamento.
  • Celular: emparelhe o app Remote a partir de um cliente em execução — consulte Emparelhar um dispositivo.

Para sincronizar o calendário de um espaço de trabalho sem nenhuma GUI conectada, use o fluxo de código de dispositivo:

Janela do terminal
GOOGLE_OAUTH_CLIENT_ID=<id> GOOGLE_OAUTH_CLIENT_SECRET=<secret> \
./build/cli/<os_arch>/bundle/bin/cc_server calendar connect \
--data-dir ./data --workspace <workspace-id>

Ele imprime um código e uma URL para aprovar em outro dispositivo, armazena o refresh token no servidor e encerra. Um build a partir do código-fonte não traz cliente Google, então as variáveis de ambiente de id e secret são obrigatórias — não existe flag --google-client-id. Consulte Conectar um Google Calendar.

As flags usadas acima são as de que uma primeira configuração precisa. A superfície completa — 23 pares de flag e variável de ambiente, os quatro subcomandos e o layout do diretório de dados — está na referência da CLI do cc_server.

As quatro que você provavelmente vai precisar:

Flag Padrão Significado
--data-dir o diretório de dados de aplicativo por usuário do SO Bancos de dados, segredos, modelos e mídia em cache
--port 9030 Porta TCP (0 = efêmera)
--bind loopback loopback, ou any para todas as interfaces (precisa de TLS ou --insecure)
--public-url derivado do bind A URL de RPC anunciada aos clientes emparelhados. Defina-a explicitamente atrás de um proxy, NAT ou túnel

O diretório de dados padrão é ~/Library/Application Support/control-center no macOS, %APPDATA%\control-center no Windows e $XDG_DATA_HOME/control-center (ou então ~/.local/share/control-center) nos demais. Um .cc_server relativo ao cwd só é usado quando nenhum diretório home ou de dados de aplicativo é resolvido.

O cc_server é toda a superfície do produto, não um subconjunto: o caminho de dados e as assinaturas de repo-RPC, o endpoint de ferramentas MCP em POST /mcp e GET /sse na mesma porta, webhooks de entrada, os proxies de mídia e de fontes, callbacks de logon único, o protocolo de lease da frota, a busca de RSS do lado do servidor em um agendamento de 30 minutos e a busca vetorial (a extensão sqlite_vector é registrada como auto-extension global do processo, degradando para busca de texto completo apenas onde a extensão não está disponível).

O dispatcher é sem estado: não existe vínculo de espaço de trabalho por sessão. Toda chamada com escopo de espaço de trabalho carrega o próprio workspace_id e o acesso é decidido pela associação e pelo papel do chamador no espaço de trabalho — não por deter uma chave de pareamento.

Duas coisas que você precisa saber antes de expor a porta além do loopback:

  • O MCP falha fechado fora do host. Uma superfície MCP sem token responde apenas ao loopback. Vincular any não a amplia — uma requisição de fora do host é recusada com “MCP requires a bearer token to answer non-loopback clients” até você configurar um (mcp.setToken, ou Configurações → Servidor → Servidores MCP).
  • GET /healthz não tem autenticação por design, com CORS curinga, e publica o id, o nome, a impressão digital de identidade e a versão de build do servidor. Os clientes precisam da impressão digital para fixar o servidor, então isso é deliberado — mas significa que qualquer cc_server alcançável divulga tudo isso a quem perguntar.