Gå til innholdet

Drift og klienter

Control Center er ikke ett program. Det er en tynnklientarkitektur: en headless server som eier dataene dine, og et sett klienter som tegner et grensesnitt over én enkelt RPC-tilkobling til den. Å forstå denne delingen forklarer hvorfor noe kjører der det gjør, og hva hver klient kan og ikke kan nå.

En prosess som heter cc_server holder databasene — en liten global.db for tilstand på tvers av hele serveren, pluss én SQLite-fil per arbeidsområde, åpnet lat — og kjører det langvarige arbeidet: pipeline-motorer, avstemmere, MCP-verktøyflaten og orkestreringlytteren. Alt som må overleve et vindu, eller som to klienter skal se identisk, bor der.

Ingen klient åpner en databasefil selv. I stedet holder hver klient én RemoteRpcClient og utfører repo-operasjoner og abonnementer over den. Serveren autentiserer tilkoblingen mot en forhåndsdelt nøkkel for parede enheter — og slutter deretter å bry seg om tilkoblingen. En økt er ikke bundet til et arbeidsområde. Dispatcheren er tilstandsløs: hvert arbeidsområde-avgrensede kall bærer sin egen workspace_id i argumentene, og et kall som utelater det, avvises.

Det er med vilje, og det er det som lar to klienter på én server sitte i ulike arbeidsområder uten et serverlagret «gjeldende arbeidsområde» de kan være uenige om. Isolasjon håndheves i stedet per kall, i fire trinn:

  1. Operasjonen erklærer om den er arbeidsområde-avgrenset; er den det og ingen workspace_id kom med, avvises kallet.
  2. Id-en som angis, må være et registrert arbeidsområde, sjekket før noe åpner en database — ellers ville en ukjent id skape en tom spøkelses-workspace.db ved hver forespørsel. En id som ikke finnes, er et «ikke funnet».
  3. Kallers WorkspaceRole må oppfylle operasjonens minstekrav, utledet fra typen: lesing krever gjest, endring krever medlem, destruktive operasjoner krever admin.
  4. En operasjon som eksponerer repo-innhold, krever i tillegg en tilgang per repo på nivået den erklærer — none / read / review / write — slik at arbeidsområdemedlemskap aldri stilltiende gir flere rettigheter enn kodeverten.

Setningen som betyr noe: å ha en paringsnøkkel er ikke tilgangsgrensen; medlemskap er det.

Tre press førte mot en server:

  1. Én kilde til sannhet. En skrivebordsapp og en telefon, eller en bærbar og en stasjonær, bør se samme flåtetilstand. Hvis hver av dem eide sin egen database, ville de gli fra hverandre. Én server gjør at hver klient leser de samme levende dataene.
  2. Nettet kan ikke betjene seg selv. En nettleser kan ikke starte en delprosess eller åpne en lokal fil, så et nettbygg kan aldri eie databasen eller kjøre en agent. Det må snakke med en server som gjør det. I stedet for å bygge en andre datasti kun for nettet, ble skrivebordsappen ført inn på den samme stien.
  3. Headless drift. En server du kan kjøre på en liten boks — tilgjengelig fra en telefon på toget eller en nettleser hvor som helst — er nyttigere enn et GUI du må la stå åpent.

Avveiningen er at serveren nå er en avhengighet: kjører den ikke, fungerer ingenting. Skrivebordsappens standardsvar på det er å starte en selv.

Fem klienter når serveren, hver med en annen form og tillitsprofil.

Flutter-skrivebordsapplikasjonen (macOS, Windows, Linux) er fullklienten. Ved første oppstart spør den hvordan den skal kjøre, og husker deretter valget:

  • Lokal: start og overvåk en cc_server på denne maskinen — standardoppsettet, selvstendig og for én bruker. Skrivebordsappen starter serveren, som eier databasen under app-support-roten og snakker med den over loopback.
  • Ekstern: koble til en cc_server som kjører et annet sted, over en sikker WebSocket. Dataene ligger på den serveren; denne skrivebordsappen er bare en gjengiver.

I begge tilfeller erstatter den resulterende RPC-klienten appens dataleverandør, slik at hele brukergrensesnittet — hver skjerm, hver funksjon — leser og skriver gjennom serveren. Skrivebordsappen tar aldri på databasefilen direkte.

Nettbygget er den samme Flutter-appen kompilert for å kjøre i en nettleser, hostet på app.usectrl.dev, så det er ingenting å installere. En nettleser kan ikke starte en delprosess, så nettklienten er alltid ekstern: den ringer opp en cc_server over wss:// og tegner hele skrivebordsgrensesnittet. Når den er tilkoblet, viser den de samme skjermene mot den samme flåten, fordi det er den samme koden over den samme RPC-en. Hullene er de få stedene som trenger en innebygd egenskap nettleseren ikke har — kvalitetslaget for møteopptak nedenfor er det viktigste, og varsler fra operativsystemet er et annet.

Dette er hvorfor «ingen nedlasting» og «ingen server» er to forskjellige påstander. Nettklienten fjerner installasjonen; den fjerner ikke serveren. Det den ringer opp, kan være en cc_server på den samme bærbare maskinen — ws://localhost:9030, som ikke trenger sertifikat fordi nettlesere regner loopback som pålitelig — eller en på en annen maskin, som må være tilgjengelig over wss:// med et sertifikat nettleseren stoler på. cc_server nekter å binde en adresse utenfor loopback uten TLS, så en ekstern server betyr enten serverens eget sertifikat, en TLS-avsluttende proxy, eller en tunnel eller VPN som skaffer ett.

Selve nettpakken hostes separat fra serveren den snakker med. En frittstående cc_server betjener RPC-endepunktet, MCP-flaten og proksyene sine, men ikke en nettpakke — den statiske filstien finnes i serveren, men ingen webrot er konfigurert på den frittstående binærfilen, så den har ingenting å betjene på /. Den publiserte containeravbildningen cc-webapp er det som hoster nettpakken, og scripts/build_web.sh er det som bygger den. En server godtar bare nettleser-origins som står på --allowed-origins-listen, som standard er https://app.usectrl.dev, så egen hosting av nettpakken betyr å legge til dens origin.

Medier (avatarer, bilder i strømmen, PR-skjermbilder) går gjennom serverens proxy-endepunkt i stedet for å treffe oppstrømsverter direkte, slik at en nettleser aldri henter en vilkårlig origin. Den utrullede klienten setter en per-forespørsel Content-Security Policy ut fra en ikke-sensitiv informasjonskapsel, slik at bare origin til den tilkoblede serveren tillates.

Remote-appen er en egen, lettere klient — en Flutter-nett-PWA på remote.usectrl.dev. Den er med vilje ikke hele appen. Den pares over et meglet WebSocket-relé med ende-til-ende-forsegling og snakker en overveiende lesende del av verktøyflaten: les saker, meldinger og nyhetsstrømmen; send et svar eller oppdater en sak.

En telefon er en aktor med lavere privilegier enn en lokal agent. En verktøypolicy med standardavvisning, en forhåndsdelt nøkkel som forsegler hver ramme, en øktkapabilitet som stenger privilegerte operasjoner (for eksempel parring) ute fra en følgesvennklient, sjekkene av rolle og repo-tilgang per kall beskrevet ovenfor, og hastighetsbegrensning hindrer at en godkjent (eller lekket) parring blir full fjernstyring. Se Fjernstyring og mobil for hele sikkerhetsmodellen.

Eksterne verktøy — editorer, andre agenter, CLI-er — som snakker Model Context Protocol, kobler til serverens MCP-flate og bruker det typede verktøyregisteret. Dette er den programmerbare integrasjonsveien; det er ikke et GUI. Se Bruk MCP-serveren og MCP-verktøyreferansen.

En cc_worker er en headless ren Dart-binærfil som pares med en cc_server, erklærer vertens kapabiliteter og henter leide jobber for utførelse. Den strømmer prosesshendelser tilbake over den samme RPC-en og holder ingen varig tilstand: ingen database, ingen autentisering, ingen godkjenninger, ingen budsjetter — de forlater aldri cc_server. En worker er en lem, ikke en ekstra hjerne. Én autoritativ server, N dumme lemmer; det finnes ingen konsensus, ingen trafikk worker til worker.

Leaset utførelse er fortsatt bare lease-infrastruktur. agentRun er den eneste typen som kjører en ekte kommando (CC_JOB_COMMAND, eller en ekkoet prompt hvis ingen ble oppgitt). pipelineStep, codeIndex, goldenRender, benchmark og evalBatch utfører foreløpig en sondering med git --version / rev-parse HEAD, slik at strømmestien blir prøvd uten å late som om disse jobbene kjørte.

Det den annonserer, er operativsystemet, CPU-arkitektur, antall kjerner, RAM, om et Flutter-SDK er tilgjengelig, og hvilke sandkasse-backender verten har (ingen på Windows). En dedikert worker annonserer også alltid alwaysOn og acceptsParallel, og det er disse planleggeren legger til grunn når den plasserer etter strategien fest → foretrekk → fordel. Det finnes ingen GPU- eller ML-sondering.

En enslig skrivebordsinstallasjon merker ingenting av dette, fordi vanlig utsending ikke går gjennom flåten i det hele tatt. Det finnes en implisitt worker i prosessen, registrert for serververten, men det er en registreringssøm uten kjørere koblet til — en jobb som faktisk plasseres på den lokale workeren, mislykkes i stedet for å kjøre. I dag er det bare en ekte cc_worker som utfører leide jobber.

Ansvarsområde Server (cc_server) Flåteutfører (cc_worker) Skrivebordsklient Nett-/telefonklient
Databaser (Drift/SQLite) eier dem — — —
Autentisering, godkjenninger, budsjetter, identitet eier dem — — —
Pipelines, avstemmere, orkestreringlytter kjører det — — —
MCP-verktøyflate betjener den — — —
Agentutførelse kjører det (eller leaser det ut) utfører leide jobber — —
UI-gjengivelse — — full full (nett) / mest lesing (telefon)
Møteopptak (mikrofon + møtelyd) — — innebygd opptak bare nett, via skjermdelingslyd
Kalender-OAuth (Google) lagrer tokener — — —

De to nivåene bruker ikke samme sandkasse. cc_server pakker kjøringene den utfører selv, inn i vertens OS-sandkasse der en backend finnes — Seatbelt på macOS, bubblewrap på Linux og WSL2 — og faller tilbake på de andre grensene på Windows eller uten bwrap og socat. cc_worker bruker ingen sandkasse i det hele tatt. Den annonserer en sandbox-kapabilitet, slik at serveren kan plassere jobber etter plattform, men ingenting i workeren tar den i bruk: en leaset jobb startes direkte, begrenset av miljøet til leasen og worktreet den fikk. Å lease ut arbeid flytter derfor utførelsen utenfor sandkassen, selv på en vert som har en. Se Sandkasse og sikkerhet for hva som begrenser en kjøring i hvert tilfelle.

Møteopptak skjer på en klient, fordi det trenger en mikrofon og møtets lyd — en server i et skap kan ikke ta opp samtalen din. Opptaket oppsummeres deretter av serverens pipeline, og notatene lagres der hver klient kan lese dem.

Nettklienten tar også opp, noe tabellens form kan skjule: den fanger mikrofonen pluss et lydspor for skjermdeling i nettleseren og strømmer 16 kHz PCM til serveren. Den krever dette lydsporet, og feiler derfor eksplisitt der nettleseren ikke tilbyr ett. Det som virkelig bare finnes på skrivebordet, er kvalitetslaget rundt opptaket — ekkoutnulling på signalnivå via den innebygde prosessoren, inngangsnivåmåleren og advarselen om død mikrofon, å kjøre en oppsummering på nytt, å avbryte behandling og kalenderens handling for opptak og kobling.

Hver klient beviser hvem den er med en forhåndsdelt nøkkel som legges inn én gang:

  • Skrivebordsappen i lokal modus genererer en fersk nøkkel ved hver oppstart og gir den til den startede serveren via miljøet, slik at ingenting hemmelig lagres på skrivebordet.
  • Eksterne klienter (ekstern skrivebordsapp, nett, telefon) bruker en nøkkel utstedt av serverens pair-kommando og lagret i OS-nøkkelringen (eller lokalt på telefonen).

Tilbakekalling er umiddelbar, ikke utsatt. Serveren overvåker tabellen over parede enheter og avslutter enhver åpen økt der enheten har forlatt det aktive settet, innen sekunder — både på den direkte WebSocket-stien og på relé-stien. Du trenger ikke vente på at enheten kobler til på nytt for å kutte den.

Nøkkelen går aldri gjennom en URL som en statisk vert kan se. Når den kommer via en lenke — på telefonen eller i en nettleser — ligger den i URL-fragmentet, delen etter #, som nettleseren aldri sender til serveren som hoster siden. Det er invarianten som er verdt å huske, og den gjelder for begge. Nettklienten har to andre innganger: tilkoblingsskjemaet, der du limer inn en server og en nøkkel direkte, og en invitasjonskode, som den løser inn mot en legitimasjon utstedt bare for den nettleseren.