Hoppa till innehåll

Driftsättning och klienter

Control Center är inte ett enda program. Det är en arkitektur med tunna klienter: en headless-server som äger din data och en uppsättning klienter som renderar ett gränssnitt över en enda RPC-anslutning till den. Att förstå den här uppdelningen förklarar varför vissa saker körs där de körs och vad varje klient kommer åt – och inte kommer åt.

En process som heter cc_server håller databaserna – en liten global.db för serverövergripande tillstånd plus en SQLite-fil per arbetsyta, som öppnas vid behov – och kör det långlivade arbetet: pipelinemotorer, reconcilers, MCP-verktygsytan och orkestreringslyssnaren. Allt som behöver leva längre än ett fönster, eller som två klienter ska se identiskt, bor där.

Ingen klient öppnar en databasfil själv. I stället håller varje klient en enda RemoteRpcClient och utfärdar repo-operationer och prenumerationer över den. Servern autentiserar anslutningen mot en i förväg delad nyckel för den parkopplade enheten – och slutar sedan bry sig om anslutningen. En session är inte bunden till en arbetsyta. Dispatchern är tillståndslös: varje anrop som gäller en arbetsyta bär sitt eget workspace_id i argumenten, och ett anrop som utelämnar det avvisas.

Det är avsiktligt, och det är vad som låter två klienter på en server sitta i olika arbetsytor utan någon serverhållen ”aktuell arbetsyta” som de kan vara oense om. I stället upprätthålls isoleringen per anrop, i fyra steg:

  1. Operationen deklarerar om den är kopplad till en arbetsyta; om den är det och inget workspace_id kom med avvisas anropet.
  2. Det angivna id:t måste vara en registrerad arbetsyta, och det kontrolleras innan något öppnar en databas – annars skulle ett okänt id materialisera en tom spökdatabas workspace.db vid varje begäran. Ett misslyckat uppslag ger not-found.
  3. Anroparens WorkspaceRole måste nå upp till den lägsta nivå operationen kräver, härledd ur dess typ: läsning kräver gäst, ändring kräver medlem, destruktiv kräver admin.
  4. En operation som exponerar repo-innehåll kräver dessutom en behörighet per repo på den nivå den anger – none / read / review / write – så att ett arbetsytemedlemskap aldrig tyst ger fler behörigheter än kodvärden.

Meningen som spelar roll: att ha en parkopplingsnyckel är inte åtkomstgränsen; medlemskapet är det.

Tre påtryckningar drev fram en server:

  1. En enda källa till sanning. En dator och en telefon, eller en laptop och en dator, ska se samma tillstånd i flottan. Om var och en hade sin egen databas skulle de glida isär. En enda server gör att varje klient läser samma livedata.
  2. Webben kan inte vara sin egen server. En webbläsare kan inte starta en subprocess eller öppna en lokal fil, så ett webbbygge kan aldrig äga databasen eller köra en agent. Det måste prata med en server som gör det. I stället för att bygga en andra, webbexklusiv dataväg fördes skrivbordsappen in på samma väg.
  3. Headless drift. En server du kan köra på en liten burk – nåbar från en telefon på tåget eller en webbläsare var som helst – är mer användbar än ett GUI du måste lämna öppet.

Avvägningen är att servern nu är ett beroende: kör den inte fungerar ingenting. Skrivbordsappens standardsvar på det är att starta en själv.

Fem klienter når servern, var och en med en egen form och egen tillitsprofil.

Flutter-appen för skrivbordet (macOS, Windows, Linux) är den fullständiga klienten. Vid första starten frågar den hur den ska köras och kommer sedan ihåg valet:

  • Lokalt: starta och övervaka en cc_server på den här maskinen – standarduppsättningen, fristående och för en enda användare. Skrivbordsappen startar servern, som äger databasen under app-support-roten och kommunicerar med den över loopback.
  • Fjärr: anslut till en cc_server som körs någon annanstans över en säker WebSocket. Datan ligger på den servern; den här skrivbordsappen är bara en renderare.

I båda fallen tar den resulterande RPC-klienten över appens dataleverantör, så hela gränssnittet – varje skärm, varje funktion – läser och skriver via servern. Skrivbordsappen rör aldrig databasfilen direkt.

Webbbygget är samma Flutter-app kompilerad för att köras i en webbläsare, och finns på app.usectrl.dev så att det inte finns något att installera. En webbläsare kan inte starta en subprocess, så webbklienten är alltid fjärransluten: den ringer upp en cc_server över wss:// och renderar hela skrivbordsgränssnittet. När den är ansluten visar den samma skärmar mot samma flotta, eftersom det är samma kod över samma RPC. Luckorna är de få ställen som behöver en nativ förmåga som en webbläsare inte har – kvalitetslagret för mötesinspelning nedan är det viktigaste, och OS-aviseringar ett annat.

Det är därför ”ingen nedladdning” och ”ingen server” inte är samma sak. Webbklienten tar bort installationen; den tar inte bort servern. Det den ringer upp kan vara en cc_server på samma laptop – ws://localhost:9030, som inte kräver något certifikat eftersom webbläsare betraktar loopback som pålitligt – eller en på en annan maskin, som måste vara nåbar över wss:// med ett certifikat som webbläsaren litar på. cc_server vägrar binda en adress som inte är loopback utan TLS, så en fjärrserver innebär antingen ett eget certifikat, en TLS-avslutande proxy, eller en tunnel eller VPN som bidrar med ett.

Själva bundlen hostas separat från servern den pratar med. En fristående cc_server betjänar RPC-ändpunkten, MCP-ytan och dess proxyservrar, men inte en webbundle – sökvägen för statiska filer finns i servern, men ingen webbrot är konfigurerad på den fristående binären, så den har inget att betjäna på /. Den publicerade containeravbildningen cc-webapp är det som hostar bundlen, och scripts/build_web.sh är det som bygger den. En server accepterar bara webbläsarorigins som finns i listan --allowed-origins, som standard https://app.usectrl.dev, så att hosta bundlen själv innebär att lägga till dess origin.

Media (avatars, flödesbilder, PR-skärmbilder) dirigeras via serverns proxyändpunkt i stället för att gå direkt till uppströmsvärdar, så en webbläsare hämtar aldrig en godtycklig origin. Den driftsatta klienten sätter en Content-Security Policy per begäran utifrån en icke-känslig cookie, så att bara den anslutna serverns origin tillåts.

Remote-appen är en separat, lättare klient – en Flutter-webb-PWA på remote.usectrl.dev. Den är medvetet inte den fullständiga appen. Den parkopplas över en förmedlad, end-to-end-förseglad WebSocket-relä och talar en mestadels läsande delmängd av verktygsytan: läsa ärenden, meddelanden och nyhetsflödet; skicka ett svar eller uppdatera ett ärende.

En telefon är en principal med lägre privilegier än en lokal agent. En verktygspolicy med neka som standard, en i förväg delad nyckel som förseglar varje frame, en sessionskapabilitet som stänger ute privilegierade operationer (till exempel parkoppling) från en kompanjonklient, roll- och repo-behörighetskontrollerna per anrop ovan samt hastighetsbegränsning hindrar en godkänd (eller läckt) parkoppling från att bli full fjärrstyrning. Se Fjärrstyrning och mobil för hela säkerhetsmodellen.

Externa verktyg – redigerare, andra agenter, CLI:er – som talar Model Context Protocol ansluter till serverns MCP-yta och konsumerar det typade verktygsregistret. Det här är integrationsvägen för program; det är inte ett GUI. Se Använd MCP-servern och MCP-verktyg.

En cc_worker är en headless-binär i ren Dart som parkopplas med en cc_server, deklarerar sin värds kapabiliteter och hämtar leasade jobb för att utföra dem. Den strömmar processhändelser tillbaka över samma RPC och håller inget varaktigt tillstånd: ingen databas, ingen autentisering, inga godkännanden, inga budgetar – de lämnar aldrig cc_server. En worker är en lem, inte en andra hjärna. En auktoritativ server, N dumma lemmar; det finns ingen konsensus, ingen trafik mellan workers.

Leasad körning är fortfarande bara lease-mekanik. agentRun är den enda typen som kör ett riktigt kommando (CC_JOB_COMMAND, eller en prompt som ekar tillbaka om ingen angavs). pipelineStep, codeIndex, goldenRender, benchmark och evalBatch gör för närvarande en sondering med git --version / rev-parse HEAD så att strömningsvägen prövas utan att låtsas att de jobben kördes.

Det den annonserar är operativsystemet, CPU-arkitekturen, antalet kärnor, RAM, om ett Flutter SDK är nåbart och vilka sandlådebackends värden har (inga på Windows). En dedikerad worker annonserar dessutom alltid alwaysOn och acceptsParallel, vilket är vad schemaläggaren pin → prefer → spill utgår från när den placerar jobb. Det finns ingen sondering av GPU eller ML.

En ensam skrivbordsapp märker inget av detta, eftersom vanlig dispatch inte går via flottan alls. Det finns en implicit worker i samma process, registrerad för servervärden, men det är bara en registreringssöm utan några runners kopplade till sig – ett jobb som faktiskt placeras på den lokala workern misslyckas i stället för att köras. I dag är det bara en riktig cc_worker som utför leasade jobb.

Aspekt Server (cc_server) Fleet-worker (cc_worker) Skrivbordsklient Webb- / telefonklient
Databaser (Drift/SQLite) äger dem — — —
Autentisering, godkännanden, budgetar, identitet äger dem — — —
Pipelines, reconcilers, orkestreringslyssnaren kör dem — — —
MCP-verktygsytan betjänar den — — —
Agentkörning kör den (eller leasar ut den) utför leasade jobb — —
Gränssnittsrendering — — full full (webb) / mestadels läsning (telefon)
Mötesinspelning (mikrofon + mötesljud) — — nativ inspelning endast webb, via skärmdelningsljud
Kalender-OAuth (Google) lagrar tokens — — —

De två nivåerna sandlådar inte på samma sätt. cc_server kör de körningar den utför själv i värdens OS-sandlåda där en backend finns – Seatbelt på macOS, bubblewrap på Linux och WSL2 – och faller tillbaka på de andra gränserna på Windows eller utan bwrap och socat. cc_worker sandlådar inte alls. Den annonserar en sandbox-kapabilitet så att servern kan placera jobb efter plattform, men inget i workern tillämpar någon: ett leasat jobb startas direkt, begränsat av leasens miljö och den worktree den fick. Att leasa ut arbete flyttar alltså körningen utanför sandlådan även på en värd som har en. Se Sandlåda och säkerhet för vad som begränsar en körning i varje fall.

Mötesinspelning sker i en klient eftersom den behöver en mikrofon och mötets ljud – en server i en garderob kan inte spela in ditt samtal. Inspelningen sammanfattas sedan av serverns pipeline och anteckningarna lagras där varje klient kan läsa dem.

Webbklienten spelar också in, vilket tabellens form kan skymma: den fångar mikrofonen plus ett ljudspår från skärmdelning i webbläsaren och strömmar 16 kHz PCM till servern. Den kräver det ljudspåret, så den misslyckas uttryckligen där webbläsaren inte kan bidra med ett. Det som verkligen är skrivbordsunikt är kvalitetslagret kring inspelningen – ekoutsläckning på signalnivå via den natives processorn, nivåmätaren för insignal och varningen för död mikrofon, att köra om en sammanfattning, att avbryta bearbetning och kalenderns åtgärd för inspelning och länkning.

Varje klient bevisar vem den är med en i förväg delad nyckel som etableras en gång:

  • Skrivbordsappen i lokalt läge genererar en ny nyckel vid varje start och skickar den till den startade servern via miljön, så inget hemligt sparas på skrivbordet.
  • Fjärrklienter (skrivbord-fjärr, webb, telefon) använder en nyckel som skapas av serverns pair-kommando och lagras i OS-nyckelringen (eller lokalt på telefonen).

Återkallning sker live, inte lat. Servern bevakar sin tabell över parkopplade enheter och stänger varje öppen session vars enhet har lämnat den aktiva mängden, inom några sekunder – både på den direkta WebSocket-vägen och på relävägen. Du behöver inte vänta på att enheten återansluter för att kapa den.

Nyckeln färdas aldrig genom en URL som en statisk host kan se. När den kommer via en länk – på telefonen eller i en webbläsare – färdas den i URL-fragmentet, delen efter #, som webbläsaren aldrig skickar till servern som hostar sidan. Det är invarianten värd att komma ihåg, och den gäller båda. Webbklienten har två andra ingångar: anslutningsformuläret, där du klistrar in en server och en nyckel direkt, och en inbjudningskod, som den löser in mot en autentiseringsuppgift som skapats enbart för den webbläsaren.