Salta ai contenuti

Deployment e client

Control Center non è un unico programma. È un’architettura thin client: un server headless che possiede i tuoi dati e un insieme di client che renderizzano un’interfaccia su un’unica connessione RPC verso di esso. Capire questa separazione spiega perché alcune cose girano dove girano e a cosa ogni client può e non può accedere.

Un processo chiamato cc_server custodisce i database — un piccolo global.db per lo stato globale del server più un file SQLite per ogni spazio di lavoro, aperto solo quando serve — ed esegue il lavoro di lunga durata: motori di pipeline, riconciliatori, la superficie degli strumenti MCP e il listener di orchestrazione. Tutto ciò che deve sopravvivere alla chiusura di una finestra, o che due client devono vedere in modo identico, risiede lì.

Nessun client apre da sé un file di database. Ogni client, invece, tiene un solo RemoteRpcClient e attraverso di esso esegue operazioni di repository e sottoscrizioni. Il server autentica la connessione contro una chiave precondivisa di dispositivo associato — e poi smette di preoccuparsi della connessione. Una sessione non è vincolata a uno spazio di lavoro. Il dispatcher è privo di stato: ogni chiamata con ambito di spazio di lavoro porta con sé il proprio workspace_id nei suoi argomenti, e una chiamata che lo omette viene rifiutata.

È una scelta deliberata, ed è ciò che permette a due client su un unico server di stare in spazi di lavoro diversi senza che il server mantenga un «spazio di lavoro corrente» sul quale possano essere in disaccordo. L’isolamento viene invece applicato a ogni chiamata, in quattro passaggi:

  1. L’operazione dichiara se ha ambito di spazio di lavoro; se ce l’ha e non è arrivato alcun workspace_id, la chiamata viene rifiutata.
  2. L’id indicato deve corrispondere a uno spazio di lavoro registrato, verificato prima che qualsiasi cosa apra un database: altrimenti un id sconosciuto materializzerebbe un workspace.db fantasma e vuoto a ogni richiesta. Una mancata corrispondenza restituisce un errore di tipo non trovato.
  3. Il WorkspaceRole di chi chiama deve raggiungere la soglia dell’operazione, derivata dal suo tipo: leggere richiede ospite, modificare richiede membro, distruggere richiede admin.
  4. Un’operazione che espone contenuto di repository richiede inoltre una concessione per repository al livello che dichiara — none / read / review / write — così l’appartenenza a uno spazio di lavoro non concede mai silenziosamente più privilegi della forge.

La frase che conta: possedere una chiave di abbinamento non è il confine di accesso; lo è l’appartenenza.

Tre pressioni hanno spinto verso un server:

  1. Un’unica fonte di verità. Un desktop e un telefono, o un portatile e un desktop, devono vedere lo stesso stato della flotta. Se ciascuno possedesse il proprio database, divergerebbero. Un unico server significa che ogni client legge gli stessi dati in tempo reale.
  2. Il web non può servire sé stesso. Un browser non può avviare un sottoprocesso né aprire un file locale, quindi una build web non potrà mai possedere il database né eseguire un agente. Deve parlare con un server che può farlo. Invece di costruire un secondo percorso dati solo per il web, il desktop è stato portato sullo stesso percorso.
  3. Funzionamento headless. Un server che puoi eseguire su una macchina piccola — raggiungibile da un telefono in treno o da un browser ovunque — è più utile di un’interfaccia grafica che devi lasciare aperta.

Il compromesso è che ora il server è una dipendenza: se non è in esecuzione, nulla funziona. La risposta predefinita del desktop è avviarne uno da sé.

Cinque client raggiungono il server, ciascuno con una forma e un profilo di fiducia diversi.

L’applicazione desktop Flutter (macOS, Windows, Linux) è il client completo. Al primo avvio chiede come deve essere eseguita, poi ricorda la scelta:

  • Locale: avvia e supervisiona un cc_server su questa macchina — la configurazione predefinita, autocontenuta e per un singolo utente. Il desktop avvia il server, che possiede il database sotto la radice di supporto dell’app e comunica con esso tramite loopback.
  • Remota: si connette a un cc_server in esecuzione altrove tramite un WebSocket sicuro. I dati risiedono su quel server; questo desktop è puramente un renderer.

In entrambi i casi il client RPC risultante sostituisce il provider di dati dell’app, così l’intera interfaccia — ogni schermata, ogni funzione — legge e scrive attraverso il server. Il desktop non tocca mai direttamente il file del database.

La build web è la stessa app Flutter compilata per girare in un browser, ospitata su app.usectrl.dev, quindi non c’è nulla da installare. Un browser non può avviare un sottoprocesso, perciò il client web è sempre remoto: contatta un cc_server tramite wss:// e renderizza l’intera interfaccia desktop. Una volta connesso mostra le stesse schermate sulla stessa flotta, perché è lo stesso codice sullo stesso RPC. Le lacune sono quel pugno di punti che richiedono una capacità nativa che un browser non ha: il livello di qualità della cattura delle riunioni descritto più avanti è il principale, e le notifiche del sistema operativo sono un altro.

Ecco perché «nessun download» e «nessun server» sono affermazioni diverse. Il client web elimina l’installazione; non elimina il server. Ciò che contatta può essere un cc_server sullo stesso portatile — ws://localhost:9030, che non richiede certificati perché i browser considerano affidabile il loopback — oppure uno su un’altra macchina, che deve essere raggiungibile tramite wss:// con un certificato di cui il browser si fida. cc_server rifiuta di associare un indirizzo non loopback senza TLS, quindi un server remoto implica il suo certificato, oppure un proxy che termina il TLS, oppure un tunnel o una VPN che ne fornisca uno.

Il bundle stesso è ospitato separatamente dal server con cui parla. Un cc_server standalone serve l’endpoint RPC, la superficie MCP e i suoi proxy, ma non un bundle web: il percorso dei file statici esiste nel server, però sul binario standalone non è configurata alcuna root web, quindi non ha nulla da servire su /. L’immagine container pubblicata cc-webapp è ciò che ospita il bundle, e scripts/build_web.sh è ciò che lo compila. Un server accetta origini dei browser solo nella sua lista --allowed-origins, che per impostazione predefinita è https://app.usectrl.dev, quindi ospitare il bundle per conto proprio significa aggiungere la sua origine.

I media (avatar, immagini del feed, screenshot delle PR) passano attraverso l’endpoint proxy del server invece di contattare direttamente gli host a monte, così un browser non scarica mai un’origine arbitraria. Il client distribuito applica a ogni richiesta una Content-Security Policy presa da un cookie non sensibile, in modo che sia consentita solo l’origine del server connesso.

L’app Remote è un client separato e più leggero: una PWA web Flutter su remote.usectrl.dev. Di proposito non è l’app completa. Si associa tramite un relay WebSocket negoziato e sigillato end-to-end, e parla una porzione prevalentemente di lettura della superficie degli strumenti: leggere ticket, messaggi e notizie; inviare una risposta o aggiornare un ticket.

Un telefono è un principal con privilegi inferiori rispetto a un agente locale. Una policy degli strumenti che nega per impostazione predefinita, una chiave precondivisa che sigilla ogni frame, una capacità di sessione che esclude un client complementare dalle operazioni privilegiate (l’associazione, ad esempio), i controlli di ruolo e di concessione per repository a ogni chiamata descritti sopra e il rate limiting impediscono che un’associazione approvata (o trapelata) diventi un controllo remoto completo. Vedi Controllo remoto e mobile per il modello di sicurezza completo.

Gli strumenti esterni — editor, altri agenti, CLI — che parlano il Model Context Protocol si connettono alla superficie MCP del server e consumano il registro tipizzato degli strumenti. È il percorso di integrazione programmatico; non è un’interfaccia grafica. Vedi Usa il server MCP e il Riferimento degli strumenti MCP.

Un cc_worker è un binario headless in puro Dart che si associa a un cc_server, dichiara le capacità del suo host e preleva lavori in lease da eseguire. Trasmette gli eventi di processo attraverso lo stesso RPC e non conserva alcuno stato durevole: niente database, niente autenticazione, niente approvazioni, niente budget: nulla di tutto ciò lascia mai cc_server. Un worker è un arto, non un secondo cervello. Un server autoritativo, N arti stupidi; non c’è consenso né traffico tra worker.

L’esecuzione in lease resta comunque impalcatura da lease. agentRun è l’unico tipo che esegue un comando reale (CC_JOB_COMMAND, oppure un prompt restituito tale e quale se non ne è stato fornito nessuno). pipelineStep, codeIndex, goldenRender, benchmark e evalBatch attualmente fanno una sonda con git --version / rev-parse HEAD, così il percorso di streaming viene esercitato senza fingere che quei lavori siano stati eseguiti.

Ciò che annuncia sono il sistema operativo, l’architettura della CPU, il numero di core, la RAM, se un SDK Flutter è raggiungibile e quali backend sandbox ha l’host (nessuno su Windows). Un worker dedicato annuncia inoltre sempre alwaysOn e acceptsParallel, ed è su questo che si basa lo scheduler pin → prefer → spill per il posizionamento. Non c’è alcuna sonda GPU o ML.

Un desktop isolato non nota nulla di tutto questo, perché l’invio ordinario non passa affatto per la flotta. Un worker implicito in-process registrato per l’host del server esiste davvero, ma è una cucitura di registrazione senza alcun runner collegato: un lavoro effettivamente assegnato al worker locale fallisce invece di essere eseguito. Oggi solo un vero cc_worker esegue lavori in lease.

Ambito Server (cc_server) Esecutore di flotta (cc_worker) Client desktop Client web / telefono
Database (Drift/SQLite) li possiede — — —
Autenticazione, approvazioni, budget, identità la possiede — — —
Pipeline, riconciliatori, listener di orchestrazione li esegue — — —
Superficie degli strumenti MCP la serve — — —
Esecuzione degli agenti la esegue (o la cede in lease) esegue i lavori in lease — —
Rendering dell’interfaccia — — completo completo (web) / prevalentemente in lettura (telefono)
Cattura delle riunioni (microfono + audio della riunione) — — cattura nativa solo web, tramite l’audio di condivisione schermo
OAuth del calendario (Google) conserva i token — — —

I due livelli non applicano lo stesso sandbox. cc_server avvolge le esecuzioni che esegue da sé nel sandbox del sistema operativo dell’host dove esiste un backend — Seatbelt su macOS, bubblewrap su Linux e WSL2 — e ricorre agli altri confini su Windows o in assenza di bwrap e socat. cc_worker non applica alcun sandbox. Annuncia una capacità sandbox così il server può posizionare i lavori in base alla piattaforma, ma nulla nel worker ne applica uno: un lavoro in lease viene avviato direttamente, limitato dall’ambiente del lease e dal worktree che gli è stato assegnato. Cedere lavoro all’esterno sposta quindi l’esecuzione fuori dal sandbox anche su un host che ne ha uno. Vedi Sandbox e sicurezza per ciò che delimita un’esecuzione in ciascun caso.

La cattura delle riunioni avviene su un client perché richiede un microfono e l’audio della riunione: un server chiuso in un armadio non può registrare la tua chiamata. La registrazione viene poi riassunta dalla pipeline del server e le note vengono salvate dove ogni client può leggerle.

Anche il client web registra, cosa che la forma della tabella può nascondere: cattura il microfono più una traccia audio di condivisione schermo nel browser e trasmette PCM a 16 kHz al server. Richiede quella traccia audio, quindi fallisce esplicitamente dove il browser non ne fornisce una. Ciò che è davvero esclusivo del desktop è il livello di qualità attorno alla cattura: la cancellazione dell’eco a livello di segnale tramite il processore nativo, il misuratore di livello d’ingresso e l’avviso di microfono muto, rieseguire un riassunto, annullare l’elaborazione e l’azione di registra e collega del calendario.

Ogni client prova la propria identità con una chiave precondivisa fornita una volta sola:

  • Il desktop in modalità locale genera una chiave nuova a ogni avvio e la passa al server avviato tramite l’ambiente, così sul desktop non viene persistito nulla di segreto.
  • I client remoti (desktop remoto, web, telefono) usano una chiave emessa dal comando pair del server e conservata nel portachiavi del sistema operativo (o, sul telefono, localmente).

La revoca è immediata, non pigra. Il server tiene sotto controllo la sua tabella dei dispositivi associati e chiude qualsiasi sessione aperta il cui dispositivo abbia lasciato l’insieme attivo, entro pochi secondi: sia sul percorso WebSocket diretto sia su quello del relay. Non devi aspettare che il dispositivo si riconnetta per tagliarlo fuori.

La chiave non viaggia mai in un URL che un host statico possa vedere. Quando arriva tramite link — sul telefono o in un browser — viaggia nel frammento dell’URL, la parte dopo #, che il browser non invia mai al server che ospita la pagina. Questa è l’invariante da ricordare, e vale per entrambi. Il client web ha altri due punti di ingresso: il modulo di connessione, dove incolli direttamente server e chiave, e un codice di invito, che riscatta con una credenziale emessa solo per quel browser.

  • Spazi di lavoro e isolamento: perché ogni chiamata RPC porta il proprio id di spazio di lavoro e come la separazione dei database rende strutturale l’isolamento
  • Architettura: come è strutturato il codice attorno al server e ai suoi client
  • Controllo remoto e mobile: l’associazione del telefono e il modello di sicurezza