Deployment en clients
Control Center is geen enkel programma. Het is een thin-clientarchitectuur: een headless server die je gegevens bezit, plus een reeks clients die daarover via één RPC-verbinding een interface tekenen. Deze splitsing verklaart waarom sommige dingen draaien waar ze draaien en wat elke client wel en niet kan bereiken.
De server bezit de gegevens
Sectie getiteld “De server bezit de gegevens”Een proces met de naam cc_server houdt de databases bij: een kleine
global.db voor serverbrede status plus één SQLite-bestand per
werkruimte, dat pas wordt geopend
wanneer het nodig is. Daar draait het langdurige werk: pipeline-engines,
reconcilers, het MCP-tooloppervlak en de orkestratie-listener. Alles wat een
venster moet overleven, of wat twee clients identiek moeten zien, leeft daar.
Geen enkele client opent zelf een databasebestand. In plaats daarvan houdt elke
client één RemoteRpcClient vast en stuurt daarover repo-operaties en
abonnementen. De server authenticeert de verbinding aan de hand van een
koppelingssleutel van het gekoppelde apparaat — en laat de verbinding daarna
los. Een sessie is niet aan een werkruimte gebonden. De dispatcher is
stateloos: elke werkruimtegebonden aanroep draagt zijn eigen workspace_id mee
in zijn argumenten, en een aanroep die die weglaat wordt geweigerd.
Dat is bewust, en het is wat twee clients op één server in verschillende werkruimtes laat zitten, zonder dat de server een “huidige werkruimte” bijhoudt waarover ze het oneens kunnen zijn. Isolatie wordt in plaats daarvan per aanroep afgedwongen, in vier stappen:
- De operatie geeft aan of ze werkruimtegebonden is; is dat zo en is er geen
workspace_idmeegekomen, dan wordt de aanroep geweigerd. - Het opgegeven id moet een geregistreerde werkruimte zijn, en dat wordt
gecontroleerd voordat er ook maar iets een database opent — anders zou een
onbekend id bij elk verzoek een lege spook-
workspace.dblaten ontstaan. Een misser betekent niet gevonden. - De
WorkspaceRolevan de aanroeper moet de ondergrens van de operatie halen, afgeleid van het soort operatie: lezen vereist gast, wijzigen vereist lid, destructief vereist admin. - Een operatie die repo-inhoud blootlegt, vereist daarnaast een
per-repo-machtiging op het niveau dat ze aangeeft —
none/read/review/write— zodat lidmaatschap van een werkruimte de forge nooit stilzwijgend meer rechten geeft.
De zin die ertoe doet: het hebben van een koppelingssleutel is niet de toegangsgrens; lidmaatschap is dat.
Waarom deze splitsing
Sectie getiteld “Waarom deze splitsing”Drie krachten duwden richting een server:
- Eén bron van waarheid. Een desktop en een telefoon, of een laptop en een desktop, moeten dezelfde fleetstatus zien. Als elk zijn eigen database had, zouden ze uit elkaar lopen. Eén enkele server betekent dat elke client dezelfde livegegevens leest.
- Het web kan niet voor zichzelf zorgen. Een browser kan geen subproces starten en geen lokaal bestand openen, dus een web-build kan nooit de database bezitten of een agent uitvoeren. Hij moet praten met een server die dat wel kan. In plaats van een tweede datapad te bouwen dat alleen voor het web dient, werd de desktop op hetzelfde pad gebracht.
- Headless gebruik. Een server die je op een klein kastje kunt draaien — bereikbaar vanaf een telefoon in de trein of een browser waar dan ook — is nuttiger dan een GUI die je open moet laten staan.
De keerzijde is dat de server nu een afhankelijkheid is: als hij niet draait, werkt er niets. Het standaardantwoord van de desktop daarop is er zelf een te starten.
De clients
Sectie getiteld “De clients”Vijf clients bereiken de server, elk met een andere vorm en een ander vertrouwensprofiel.
Desktop-app
Sectie getiteld “Desktop-app”De Flutter-desktopapplicatie (macOS, Windows, Linux) is de volledige client. Bij de eerste start vraagt ze hoe ze moet draaien en onthoudt ze die keuze:
- Lokaal: start en bewaak een
cc_serverop deze machine — de standaard, op zichzelf staande opzet voor één gebruiker. De desktop start de server, die de database onder de app-supportmap bezit en er via loopback mee praat. - Extern: verbind met een
cc_serverdie elders draait, via een beveiligde WebSocket. De gegevens staan op die server; deze desktop is puur een renderer.
In beide gevallen vervangt de resulterende RPC-client de dataprovider van de app, zodat de hele UI — elk scherm, elke functie — via de server leest en schrijft. De desktop raakt het databasebestand nooit rechtstreeks aan.
Webclient
Sectie getiteld “Webclient”De web-build is dezelfde Flutter-app, gecompileerd om in een browser te draaien
en gehost op app.usectrl.dev, zodat er niets te
installeren valt. Een browser kan geen subproces starten, dus de webclient is
altijd extern: hij kiest een cc_server via wss:// en rendert de
volledige desktopinterface. Eenmaal verbonden toont hij dezelfde schermen tegen
dezelfde fleet, want het is dezelfde code via hetzelfde RPC. De hiaten zijn de
paar plekken die een native mogelijkheid nodig hebben die een browser niet
heeft — de kwaliteitslaag voor vergaderopname hieronder is de belangrijkste, en
OS-meldingen zijn een tweede.
Daarom zijn “geen download” en “geen server” twee verschillende beweringen. De
webclient haalt de installatie weg; de server niet. Wat hij kiest kan een
cc_server op dezelfde laptop zijn — ws://localhost:9030, die geen
certificaat nodig heeft omdat browsers loopback als betrouwbaar beschouwen —
of een op een andere machine, die bereikbaar moet zijn via wss:// met een
certificaat dat de browser vertrouwt. cc_server weigert een
niet-loopbackadres zonder TLS te binden, dus een externe server betekent ofwel
zijn eigen certificaat, ofwel een TLS-beëindigende proxy, ofwel een tunnel of
VPN die er een levert.
De bundle zelf wordt apart gehost van de server waarmee hij praat. Een
zelfstandige cc_server bedient het RPC-eindpunt, het MCP-oppervlak en zijn
proxy’s, maar niet een web-bundle — het pad voor statische bestanden
bestaat in de server, maar op de zelfstandige binary is geen web-root
geconfigureerd, dus heeft hij niets te serveren op /. De gepubliceerde
containerimage cc-webapp host de bundle, en scripts/build_web.sh bouwt hem.
Een server accepteert browser-origins alleen op zijn lijst --allowed-origins,
die standaard https://app.usectrl.dev bevat, dus wie de bundle zelf host, moet
zijn origin toevoegen.
Media (avatars, feedafbeeldingen, PR-screenshots) lopen via het proxy-eindpunt van de server in plaats van rechtstreeks de upstream-hosts aan te spreken, zodat een browser nooit een willekeurige origin ophaalt. De uitgerolde client zet per verzoek een Content-Security Policy op basis van een niet-gevoelige cookie, zodat alleen de origin van de verbonden server is toegestaan.
Telefoonpartner (cc_remote)
Sectie getiteld “Telefoonpartner (cc_remote)”De Remote-app is een aparte, lichtere client — een Flutter-web-PWA op remote.usectrl.dev. Hij is bewust niet de volledige app. Hij koppelt via een bemiddelde WebSocket-relay waarvan de frames end-to-end verzegeld zijn, en spreekt een overwegend lezend deel van het tooloppervlak: tickets, berichten en de nieuwsfeed lezen; een antwoord sturen of een ticket bijwerken.
Een telefoon is een principal met minder rechten dan een lokale agent. Een toolbeleid met default-deny, een koppelingssleutel die elk frame verzegelt, een sessiecapability die geprivilegieerde operaties (bijvoorbeeld koppelen) uit een begeleidende client houdt, de rol- en repo-machtigingscontroles per aanroep hierboven en een limiet op de aanroepsnelheid zorgen ervoor dat een goedgekeurde (of uitgelekte) koppeling geen volledige afstandsbediening wordt. Zie Afstandsbediening en mobiel voor het volledige beveiligingsmodel.
MCP-clients
Sectie getiteld “MCP-clients”Externe tools — editors, andere agents, CLI’s — die het Model Context Protocol spreken, verbinden met het MCP-oppervlak van de server en gebruiken de getypeerde toolregistry. Dit is het pad voor programmatische integratie; het is geen GUI. Zie De MCP-server gebruiken en de MCP-tools-referentie.
Fleet-workers (cc_worker)
Sectie getiteld “Fleet-workers (cc_worker)”Een cc_worker is een headless binary in puur Dart die zich koppelt met een
cc_server, de mogelijkheden van zijn host meldt en geleasede taken ophaalt om
uit te voeren. Hij streamt procesgebeurtenissen terug via hetzelfde RPC en houdt
geen duurzame staat bij: geen database, geen authenticatie, geen
goedkeuringen, geen budgetten — die verlaten cc_server nooit. Een worker is
een arm, geen tweede brein. Eén gezaghebbende server, N domme armen; er is geen
consensus en geen verkeer tussen workers.
Uitvoering via leases is voorlopig nog alleen leidingwerk. agentRun is de
enige soort die een echt commando uitvoert (CC_JOB_COMMAND, of een
teruggekaatste prompt als er geen is meegegeven). pipelineStep, codeIndex,
goldenRender, benchmark en evalBatch sonderen momenteel met
git --version / rev-parse HEAD, zodat het streampad wordt doorlopen zonder
te doen alsof die taken zijn uitgevoerd.
Wat hij meldt is het besturingssysteem, de CPU-architectuur, het aantal cores,
het RAM, of een Flutter-SDK bereikbaar is en welke sandboxbackends de host heeft
(geen op Windows). Een toegewijde worker meldt daarnaast altijd alwaysOn en
acceptsParallel, en daarop plaatst de scheduler in de volgorde pin → prefer →
spill. Naar GPU of ML wordt niet gezocht.
Een desktop die alleen draait merkt hier niets van, omdat gewone dispatch
helemaal niet via de fleet loopt. Er is wel een impliciete worker in het
proces geregistreerd voor de host van de server, maar dat is een
registratienaad waar geen runners aan vastzitten — een taak die daadwerkelijk op
de lokale worker terechtkomt, mislukt in plaats van uit te voeren. Vandaag voert
alleen een echte cc_worker geleasede taken uit.
Wat draait waar
Sectie getiteld “Wat draait waar”| Onderdeel | Server (cc_server) |
Fleet-worker (cc_worker) |
Desktopclient | Web-/telefoonclient |
|---|---|---|---|---|
| Databases (Drift/SQLite) | bezit ze | — | — | — |
| Authenticatie, goedkeuringen, budgetten, identiteit | bezit ze | — | — | — |
| Pipelines, reconcilers, orkestratie-listener | voert ze uit | — | — | — |
| MCP-tooloppervlak | serveert het | — | — | — |
| Agentuitvoering | voert ze uit (of besteedt ze uit via leases) | voert geleasede taken uit | — | — |
| UI-rendering | — | — | volledig | volledig (web) / overwegend lezend (telefoon) |
| Vergaderopname (microfoon + vergaderaudio) | — | — | native opname | alleen web, via audio van schermdeling |
| Agenda-OAuth (Google) | slaat tokens op | — | — | — |
De twee niveaus pakken sandboxing niet hetzelfde aan. cc_server hult de runs
die hij zelf uitvoert in de OS-sandbox van de host waar een backend bestaat —
Seatbelt op macOS, bubblewrap op Linux en WSL2 — en valt op Windows of zonder
bwrap en socat terug op de andere grenzen. cc_worker sandboxt helemaal
niet. Hij meldt een sandbox-capability zodat de server taken per platform
kan plaatsen, maar niets in de worker past er een toe: een geleasede taak wordt
rechtstreeks gestart, begrensd door de omgeving van de lease en de worktree die
hij meekreeg. Werk uitbesteden via leases verplaatst de uitvoering dus buiten de
sandbox, zelfs op een host die er een heeft. Zie Sandbox en
beveiliging voor wat een
run in elk geval begrenst.
Vergaderopname gebeurt op een client, omdat die een microfoon en de audio van de vergadering nodig heeft — een server in een kast kan je gesprek niet opnemen. De opname wordt daarna samengevat door de pipeline van de server, en de notities worden bewaard waar elke client ze kan lezen.
De webclient neemt ook op, wat de vorm van de tabel kan verbergen: hij legt in de browser de microfoon plus een audiotrack van de schermdeling vast en streamt 16 kHz PCM naar de server. Die audiotrack is verplicht, dus hij faalt expliciet waar de browser er geen levert. Wat echt alleen op de desktop kan, is de kwaliteitslaag rond de opname — echounterdrukking op signaalniveau via de native processor, de ingangspegelmeter en de waarschuwing voor een dode microfoon, een samenvatting opnieuw laten lopen, de verwerking annuleren en de agenda-actie voor opnemen en koppelen.
Koppeling en vertrouwen
Sectie getiteld “Koppeling en vertrouwen”Elke client bewijst wie hij is met een koppelingssleutel die één keer wordt ingericht:
- De desktop in lokale modus genereert bij elke start een nieuwe sleutel en geeft die via de omgeving door aan de gestarte server, zodat er op de desktop niets geheims wordt bewaard.
- Externe clients (desktop op afstand, web, telefoon) gebruiken een sleutel die
door het
pair-commando van de server is uitgegeven en in de sleutelhanger van het besturingssysteem wordt bewaard (of, op de telefoon, lokaal).
Intrekken gebeurt direct, niet uitgesteld. De server houdt de tabel met gekoppelde apparaten in de gaten en verbreekt elke open sessie waarvan het apparaat de actieve set heeft verlaten, binnen enkele seconden — zowel op het directe WebSocket-pad als op het relaypad. Je hoeft niet te wachten tot het apparaat opnieuw verbindt om het af te snijden.
De sleutel reist nooit door een URL die een statische host kan zien. Als hij via
een link aankomt — op de telefoon of in een browser — reist hij mee in het
URL-fragment, het deel na #, dat de browser nooit naar de server stuurt
die de pagina host. Dat is de invariant die je moet onthouden, en hij geldt voor
beide. De webclient heeft nog twee andere ingangen: het verbindingsformulier,
waar je een server en sleutel rechtstreeks plakt, en een uitnodigingscode, die
hij inwisselt voor een referentie die alleen voor die browser is uitgegeven.
Gerelateerde concepten
Sectie getiteld “Gerelateerde concepten”- Werkruimtes en isolatie: waarom elke RPC-aanroep zijn eigen werkruimte-id meedraagt en hoe de databasesplitsing isolatie structureel maakt
- Architectuur: hoe de codebase is opgebouwd rond de server en zijn clients
- Afstandsbediening en mobiel: het koppelings- en beveiligingsmodel voor de telefoon
Gerelateerde gidsen
Sectie getiteld “Gerelateerde gidsen”- Een headless server draaien: start
cc_serveren koppel een client - Verbinden met een externe server: richt de desktop- of webclient op een server elders
- Een apparaat koppelen: voeg een telefoon, een tweede browser of een andere desktop toe
- Een fleet-worker draaien: voeg een
cc_workertoe die geleasede taken ophaalt