Sari la conținut

Implementare și clienți

Control Center nu este un singur program. Este o arhitectură thin-client: un server headless care deține datele tale și un set de clienți care randează o interfață peste o singură conexiune RPC la acesta. Înțelegerea acestei separări explică de ce unele lucruri rulează acolo unde rulează și la ce poate, respectiv la ce nu poate ajunge fiecare client.

Un proces numit cc_server deține bazele de date — un global.db mic pentru starea la nivel de server plus câte un fișier SQLite per spațiu de lucru, deschis leneș — și rulează munca de durată: motoarele de pipeline, procesele de reconciliere, suprafața de instrumente MCP și listenerul de orchestrare. Tot ce trebuie să supraviețuiască închiderii unei ferestre sau ce trebuie să arate identic pentru doi clienți se află acolo.

Niciun client nu deschide singur un fișier de bază de date. În schimb, fiecare client deține un RemoteRpcClient și emite prin el operațiuni pe repo-uri și abonamente. Serverul autentifică conexiunea pe baza unei chei prepartajate de dispozitiv asociat — iar apoi nu se mai preocupă de conexiune. O sesiune nu este legată de un spațiu de lucru. Dispecerul este fără stare: fiecare apel care vizează un spațiu de lucru își transmite propriul workspace_id în argumente, iar un apel care îl omite este respins.

Acest lucru este intenționat și tocmai de aceea doi clienți de pe același server pot lucra în spații de lucru diferite fără un „spațiu de lucru curent” ținut de server, despre care să nu fie de acord. Izolarea este impusă în schimb la fiecare apel, în patru pași:

  1. Operațiunea declară dacă vizează un spațiu de lucru; dacă da și nu a sosit niciun workspace_id, apelul este respins.
  2. Identificatorul numit trebuie să fie un spațiu de lucru înregistrat, verificat înainte ca ceva să deschidă o bază de date — altfel un id necunoscut ar materializa la fiecare cerere un workspace.db fantomă, gol. O ratare înseamnă „negăsit”.
  3. WorkspaceRole al apelantului trebuie să atingă pragul minim al operațiunii, derivat din tipul acesteia: citirea cere guest, modificarea cere member, distrugerea cere admin.
  4. O operațiune care expune conținut din repo necesită în plus o autorizare per repo, la nivelul pe care îl declară — none / read / review / write — astfel încât apartenența la spațiul de lucru să nu acorde niciodată în mod silențios mai multe privilegii decât hostul de cod.

Propoziția care contează: deținerea unei chei de asociere nu reprezintă granița de acces; apartenența o reprezintă.

Trei presiuni au împins către un server:

  1. O singură sursă de adevăr. Un desktop și un telefon, sau un laptop și un desktop, ar trebui să vadă aceeași stare a flotei. Dacă fiecare ar deține propria bază de date, ar diverge. Un singur server înseamnă că fiecare client citește aceleași date live.
  2. Webul nu se poate descurca singur. Un browser nu poate lansa un subproces și nu poate deschide un fișier local, așa că o versiune web nu poate deține niciodată baza de date și nu poate rula un agent. Trebuie să comunice cu un server care face asta. În loc să construiască o a doua cale de date, doar pentru web, desktopul a fost adus pe aceeași cale.
  3. Operare headless. Un server pe care îl poți rula pe o cutie mică — accesibil de pe un telefon din tren sau dintr-un browser de oriunde — este mai util decât o interfață grafică pe care trebuie să o lași deschisă.

Compromisul este că serverul devine acum o dependență: dacă nu rulează, nimic nu funcționează. Răspunsul implicit al desktopului la asta este să lanseze el însuși unul.

Cinci clienți ajung la server, fiecare cu o formă și un profil de încredere diferite.

Aplicația desktop Flutter (macOS, Windows, Linux) este clientul complet. La prima pornire întreabă cum ar trebui să ruleze, apoi reține alegerea:

  • Local: lansează și supraveghează un cc_server pe această mașină — configurația implicită, autonomă, pentru un singur utilizator. Desktopul pornește serverul, care deține baza de date în rădăcina de suport a aplicației și comunică cu el prin loopback.
  • Remote: conectează-te la un cc_server care rulează în altă parte printr-un WebSocket securizat. Datele se află pe acel server; acest desktop este pur și simplu un renderer.

În ambele cazuri, clientul RPC rezultat înlocuiește furnizorul de date al aplicației, astfel încât întreaga interfață — fiecare ecran, fiecare funcție — citește și scrie prin server. Desktopul nu atinge niciodată direct fișierul bazei de date.

Versiunea web este aceeași aplicație Flutter compilată pentru a rula într-un browser, găzduită la app.usectrl.dev, așa că nu ai nimic de instalat. Un browser nu poate lansa un subproces, așa că clientul web este întotdeauna remote: apelează un cc_server prin wss:// și randează întreaga interfață desktop. Odată conectat, afișează aceleași ecrane pentru aceeași flotă, pentru că este același cod peste același RPC. Lipsurile sunt cele câteva locuri care au nevoie de o capabilitate nativă pe care un browser nu o are — stratul de calitate al capturii întâlnirilor, descris mai jos, este principalul, iar notificările sistemului de operare sunt altul.

De aceea „fără descărcare” și „fără server” sunt afirmații diferite. Clientul web elimină instalarea; nu elimină serverul. Ceea ce apelează poate fi un cc_server pe același laptop — ws://localhost:9030, care nu are nevoie de certificat, deoarece browserele consideră loopback-ul demn de încredere — sau unul de pe altă mașină, care trebuie să fie accesibil prin wss:// cu un certificat în care browserul are încredere. cc_server refuză să lege o adresă non-loopback fără TLS, așa că un server remote înseamnă fie propriul certificat, fie un proxy care termină TLS, fie un tunel sau un VPN care furnizează unul.

Pachetul în sine este găzduit separat de serverul cu care comunică. Un cc_server standalone servește punctul final RPC, suprafața MCP și proxy-urile sale, dar nu și un pachet web — calea pentru fișiere statice există în server, însă nicio rădăcină web nu este configurată pe binarul standalone, așa că nu are ce să servească la /. Imaginea de container publicată cc-webapp este cea care găzduiește pachetul, iar scripts/build_web.sh este cea care îl construiește. Un server acceptă origini de browser doar din lista sa --allowed-origins, care implicit este https://app.usectrl.dev, așa că auto-găzduirea pachetului înseamnă adăugarea originii acestuia.

Conținutul media (avataruri, imagini din flux, capturi de ecran din PR-uri) trece prin punctul final de proxy al serverului în loc să lovească direct gazdele din amonte, astfel încât un browser nu preia niciodată o origine arbitrară. Clientul implementat aplică, pentru fiecare cerere, o politică de securitate a conținutului generată dintr-un cookie nesensibil, astfel încât este permisă doar originea serverului conectat.

Aplicația Remote este un client separat, mai ușor — o PWA Flutter web la remote.usectrl.dev. Intenționat, nu este aplicația completă. Se asociază printr-un releu WebSocket intermediat și sigilat cap la cap și vorbește o felie preponderent de citire a suprafeței de instrumente: citește tichete, mesaje și fluxul de știri; trimite un răspuns sau actualizează un tichet.

Un telefon este un principal cu privilegii mai reduse decât un agent local. O politică de instrumente cu refuz implicit, o cheie prepartajată care sigilează fiecare cadru, o capabilitate de sesiune care scoate operațiunile privilegiate (asocierea, de exemplu) din aria unui client companion, verificările de rol și de autorizare per repo la fiecare apel, descrise mai sus, și limitarea ratei împiedică o asociere aprobată (sau scursă) să devină control remote complet. Vezi Control remote și mobil pentru modelul complet de securitate.

Instrumentele externe — editoare, alți agenți, CLI-uri — care vorbesc Model Context Protocol se conectează la suprafața MCP a serverului și consumă registrul de instrumente tipizate. Aceasta este calea de integrare programatică; nu este o interfață grafică. Vezi Folosește serverul MCP și Referința instrumentelor MCP.

Un cc_worker este un binar headless, scris în pur Dart, care se asociază cu un cc_server, își declară capabilitățile gazdă și trage joburi concesionate pentru a le executa. Transmite înapoi evenimente de proces prin același RPC și nu păstrează nicio stare durabilă: fără bază de date, fără autentificare, fără aprobări, fără bugete — acestea nu părăsesc niciodată cc_server. Un worker este un membru, nu un al doilea creier. Un singur server autoritar, N membre simple; nu există consens, nu există trafic între workeri.

Execuția concesionată rămâne totuși un mecanism de concesionare. agentRun este singurul tip care rulează o comandă reală (CC_JOB_COMMAND, sau un prompt ecou, dacă nu a fost furnizat niciunul). pipelineStep, codeIndex, goldenRender, benchmark și evalBatch sondează momentan cu git --version / rev-parse HEAD astfel încât calea de streaming este exercitată fără a pretinde că acele joburi au rulat.

Ce anunță este sistemul de operare, arhitectura CPU, numărul de nuclee, memoria RAM, dacă un SDK Flutter este accesibil și ce backend-uri de sandbox are gazda (niciunul pe Windows). Un worker dedicat anunță de asemenea întotdeauna alwaysOn și acceptsParallel, iar pe acestea se bazează planificatorul pin → prefer → spill când plasează joburi. Nu există nicio sondă pentru GPU sau ML.

Un desktop folosit în regim solo nu observă nimic din toate acestea, pentru că trimiterea obișnuită nu trece deloc prin flotă. Există totuși un worker implicit în proces, înregistrat pentru gazda serverului, dar este doar o cusătură de înregistrare, fără niciun runner conectat la ea — un job plasat efectiv pe workerul local eșuează în loc să se execute. Astăzi, doar un cc_worker real execută joburi concesionate.

Aspect Server (cc_server) Executor din flotă (cc_worker) Client desktop Client web / telefon
Baze de date (Drift/SQLite) le deține — — —
Autentificare, aprobări, bugete, identitate le deține — — —
Pipeline-uri, procese de reconciliere, listener de orchestrare le rulează — — —
Suprafața de instrumente MCP o servește — — —
Execuția agenților o rulează (sau o concesionează) execută joburi concesionate — —
Randarea interfeței — — completă completă (web) / preponderent citire (telefon)
Captura întâlnirilor (microfon + audio din întâlnire) — — captură nativă doar web, prin audio de partajare a ecranului
Calendar OAuth (Google) stochează tokenuri — — —

Cele două niveluri nu izolează la fel. cc_server înfășoară rulările pe care le execută el însuși în sandbox-ul de sistem de operare al gazdei, acolo unde există un backend — Seatbelt pe macOS, bubblewrap pe Linux și WSL2 — și revine la celelalte granițe pe Windows sau fără bwrap și socat. cc_worker nu izolează deloc. Anunță o capabilitate sandbox pentru ca serverul să poată plasa joburi în funcție de platformă, dar nimic din worker nu aplică vreuna: un job concesionat este lansat direct, delimitat de mediul concesiunii și de worktree-ul care i-a fost dat. Externalizarea muncii prin concesionare mută astfel execuția în afara sandbox-ului, chiar și pe o gazdă care are unul. Vezi Sandbox și securitate pentru ce delimitează o rulare în fiecare caz.

Captura întâlnirilor are loc pe un client, pentru că are nevoie de un microfon și de audio-ul întâlnirii — un server într-un dulap nu poate înregistra apelul tău. Înregistrarea este apoi rezumată de pipeline-ul serverului, iar notele sunt stocate acolo unde fiecare client le poate citi.

Clientul web înregistrează și el, ceea ce forma tabelului poate ascunde: captează microfonul plus o pistă audio de partajare a ecranului în browser și trimite în flux PCM la 16 kHz către server. Cere acea pistă audio, așa că eșuează explicit acolo unde browserul nu o oferă. Ce este cu adevărat exclusiv pentru desktop este stratul de calitate din jurul capturii — anularea ecoului la nivel de semnal prin procesorul nativ, indicatorul de nivel al intrării și avertismentul de microfon mort, reluarea unui rezumat, anularea procesării și acțiunea de înregistrare și legare din calendar.

Fiecare client își dovedește identitatea cu o cheie prepartajată, provisionată o singură dată:

  • Desktopul în modul local generează o cheie nouă la fiecare pornire și o transmite serverului lansat prin variabile de mediu, astfel încât nimic confidențial nu este persistat pe desktop.
  • Clienții remote (desktop remote, web, telefon) folosesc o cheie emisă de comanda pair a serverului și stocată în keychain-ul sistemului de operare (sau, pe telefon, local).

Revocarea este live, nu întârziată. Serverul supraveghează tabelul dispozitivelor asociate și închide orice sesiune deschisă al cărei dispozitiv a părăsit setul activ, în câteva secunde — atât pe calea directă WebSocket, cât și pe calea releului. Nu trebuie să aștepți ca dispozitivul să se reconecteze pentru a-l deconecta.

Cheia nu călătorește niciodată printr-un URL pe care o gazdă statică îl poate vedea. Când ajunge printr-un link — pe telefon sau într-un browser — ea călătorește în fragmentul URL, partea de după #, pe care browserul nu o trimite niciodată serverului care găzduiește pagina. Aceasta este invarianța care merită reținută și se aplică în ambele cazuri. Clientul web are alte două puncte de intrare: formularul de conectare, unde lipești direct un server și o cheie, și un cod de invitație, pe care îl răscumpără pentru o acreditare emisă doar pentru acel browser.

  • Spații de lucru și izolare: de ce fiecare apel RPC își poartă propriul id de spațiu de lucru și cum separarea bazelor de date face izolarea structurală
  • Arhitectură: cum este structurată baza de cod în jurul serverului și al clienților săi
  • Control remote și mobil: modelul de asociere a telefonului și de securitate