Przejdź do głównej zawartości

Wdrażanie i klienci

Control Center to nie jeden program. To architektura cienkiego klienta: serwer headless, który zarządza Twoimi danymi, oraz zestaw klientów, które renderują interfejs przez jedno połączenie RPC z nim. Zrozumienie tego podziału wyjaśnia, dlaczego niektóre rzeczy działają tam, gdzie działają, i do czego każdy klient ma dostęp, a do czego nie.

Proces o nazwie cc_server przechowuje bazy danych — mały plik global.db ze stanem całego serwera oraz po jednym pliku SQLite na każdy obszar roboczy, otwieranym leniwie — i wykonuje długotrwałą pracę: silniki potoków, rekonsyliatory, zestaw narzędzi MCP i nasłuch orkiestracji. Wszystko, co musi przetrwać zamknięcie okna albo co dwa klienty powinny widzieć identycznie, znajduje się właśnie tam.

Żaden klient sam nie otwiera pliku bazy danych. Zamiast tego każdy klient trzyma jeden RemoteRpcClient i wykonuje przez niego operacje na repozytoriach oraz subskrypcje. Serwer uwierzytelnia połączenie na podstawie wstępnie współdzielonego klucza sparowanego urządzenia — a potem przestaje się połączeniem przejmować. Sesja nie jest powiązana z obszarem roboczym. Dyspozytor jest bezstanowy: każde wywołanie w zakresie obszaru roboczego niesie własny workspace_id w argumentach, a wywołanie, które go pomija, zostaje odrzucone.

To celowe rozwiązanie: dzięki niemu dwa klienty na jednym serwerze mogą działać w różnych obszarach roboczych, bez przechowywanego na serwerze „bieżącego obszaru roboczego”, o który mogłyby się spierać. Izolację egzekwuje się natomiast przy każdym wywołaniu, w czterech krokach:

  1. Operacja deklaruje, czy działa w zakresie obszaru roboczego; jeśli tak, a nie dotarł żaden workspace_id, wywołanie zostaje odrzucone.
  2. Podany identyfikator musi należeć do zarejestrowanego obszaru roboczego; sprawdza się to, zanim cokolwiek otworzy bazę danych — inaczej nieznany identyfikator tworzyłby przy każdym żądaniu pusty, widmowy plik workspace.db. Brak trafienia oznacza odpowiedź „nie znaleziono”.
  3. Rola WorkspaceRole wywołującego musi spełniać minimalny próg operacji, wynikający z jej rodzaju: odczyt wymaga roli guest, modyfikacja — member, operacja destrukcyjna — admin.
  4. Operacja, która udostępnia zawartość repozytorium, wymaga dodatkowo uprawnienia na poziomie repozytorium w zadeklarowanym zakresie — none / read / review / write — dzięki czemu członkostwo w obszarze roboczym nigdy po cichu nie daje większych uprawnień niż forge.

Zdanie, które ma znaczenie: posiadanie klucza parowania nie jest granicą dostępu; jest nią członkostwo.

Ku architekturze z serwerem skłoniły trzy czynniki:

  1. Jedno źródło prawdy. Komputer i telefon albo laptop i komputer powinny widzieć ten sam stan floty. Gdyby każde miało własną bazę danych, rozjechałyby się. Jeden serwer sprawia, że każdy klient czyta te same, bieżące dane.
  2. Przeglądarka nie obsłuży się sama. Przeglądarka nie może uruchomić podprocesu ani otworzyć lokalnego pliku, więc wersja webowa nigdy nie może zarządzać bazą danych ani uruchamiać agenta. Musi rozmawiać z serwerem, który to potrafi. Zamiast budować drugą, wyłącznie webową ścieżkę dostępu do danych, aplikację desktopową wprowadzono na tę samą ścieżkę.
  3. Działanie bez interfejsu. Serwer, który można uruchomić na małym komputerze — dostępny z telefonu w pociągu albo z przeglądarki gdziekolwiek — jest użyteczniejszy niż GUI, które trzeba trzymać otwarte.

Kompromis jest taki, że serwer staje się zależnością: jeśli nie działa, nic nie działa. Domyślną odpowiedzią aplikacji desktopowej jest uruchomienie własnego serwera.

Do serwera sięga pięć klientów, każdy o innym kształcie i profilu zaufania.

Aplikacja desktopowa we Flutterze (macOS, Windows, Linux) to pełny klient. Przy pierwszym uruchomieniu pyta, jak ma działać, a potem zapamiętuje wybór:

  • Lokalnie: uruchomienie i nadzorowanie cc_server na tym komputerze — domyślna, samowystarczalna konfiguracja dla jednego użytkownika. Aplikacja desktopowa uruchamia serwer, który przechowuje bazę danych w katalogu wsparcia aplikacji i komunikuje się z nią przez loopback.
  • Zdalnie: połączenie z cc_server działającym gdzie indziej przez bezpieczny WebSocket. Dane znajdują się na tym serwerze; ten komputer jest wyłącznie rendererem.

W obu przypadkach powstały klient RPC zastępuje dostawcę danych aplikacji, więc cały interfejs — każdy ekran, każda funkcja — czyta i zapisuje przez serwer. Aplikacja desktopowa nigdy nie dotyka bezpośrednio pliku bazy danych.

Wersja webowa to ta sama aplikacja we Flutterze skompilowana do działania w przeglądarce, hostowana pod app.usectrl.dev, więc nie ma nic do zainstalowania. Przeglądarka nie może uruchomić podprocesu, dlatego klient webowy jest zawsze zdalny: łączy się z cc_server przez wss:// i renderuje pełny interfejs desktopowy. Po połączeniu pokazuje te same ekrany i tę samą flotę, bo to ten sam kod przez to samo RPC. Luki to garść miejsc wymagających natywnych możliwości, których przeglądarka nie ma — główną jest opisana niżej warstwa jakości nagrywania spotkań, a drugą powiadomienia systemowe.

Dlatego stwierdzenia „bez pobierania” i „bez serwera” to dwie różne rzeczy. Klient webowy usuwa instalację, ale nie usuwa serwera. To, z czym się łączy, może być cc_server na tym samym laptopie — ws://localhost:9030, który nie wymaga certyfikatu, bo przeglądarki uznają loopback za zaufany — albo na innym komputerze, który musi być dostępny przez wss:// z certyfikatem, któremu przeglądarka ufa. cc_server odmawia bindowania adresu innego niż loopback bez TLS, więc zdalny serwer oznacza albo własny certyfikat, albo proxy kończące TLS, albo tunel lub VPN, który go dostarcza.

Sam pakiet jest hostowany oddzielnie od serwera, z którym rozmawia. Samodzielny cc_server obsługuje punkt końcowy RPC, zestaw MCP i swoje proxy, ale nie pakiet webowy — ścieżka plików statycznych istnieje w serwerze, jednak w samodzielnym binarium nie skonfigurowano katalogu głównego stron, więc nie ma czego udostępniać pod /. Opublikowany obraz kontenera cc-webapp hostuje pakiet, a scripts/build_web.sh go buduje. Serwer przyjmuje pochodzenia przeglądarek tylko z listy --allowed-origins, która domyślnie zawiera https://app.usectrl.dev, więc samodzielny hosting pakietu wymaga dodania jego pochodzenia.

Multimedia (awatary, obrazy w newsfeedzie, zrzuty ekranu z PR-ów) przechodzą przez punkt końcowy proxy serwera, a nie trafiają bezpośrednio do hostów źródłowych, więc przeglądarka nigdy nie pobiera danych z dowolnego pochodzenia. Wdrożony klient dodaje do każdego żądania politykę Content-Security Policy pochodzącą z niesensywnego pliku cookie, dzięki czemu dozwolone jest wyłącznie pochodzenie połączonego serwera.

Aplikacja Remote to osobny, lżejszy klient — PWA we Flutterze dostępna pod remote.usectrl.dev. Celowo nie jest pełną aplikacją. Paruje się przez brokerski, szyfrowany end-to-end przekaźnik WebSocket i obsługuje głównie odczytowy wycinek zestawu narzędzi: czytanie zgłoszeń, wiadomości i newsfeedu; wysyłanie odpowiedzi lub aktualizowanie zgłoszenia.

Telefon to podmiot o niższych uprawnieniach niż lokalny agent. Domyślnie odrzucająca polityka narzędzi, wstępnie współdzielony klucz zabezpieczający każdą ramkę, uprawnienie sesji, które wyklucza uprzywilejowane operacje (na przykład parowanie) z aplikacji towarzyszącej, opisane wyżej kontrole roli i uprawnień do repozytoriów przy każdym wywołaniu oraz ograniczanie tempa sprawiają, że zatwierdzone (lub ujawnione) parowanie nie staje się pełnym zdalnym sterowaniem. Pełny model bezpieczeństwa opisano w sekcji Zdalne sterowanie i mobile.

Zewnętrzne narzędzia — edytory, inni agenci, narzędzia CLI — które mówią protokołem Model Context Protocol, łączą się z zestawem MCP serwera i korzystają z typowanego rejestru narzędzi. To programistyczna ścieżka integracji, a nie GUI. Zobacz Używaj serwera MCP i dokumentację referencyjną narzędzi MCP.

cc_worker to napisane w czystym Darcie binarium headless, które paruje się z cc_server, deklaruje możliwości swojego hosta i pobiera wydzierżawione zadania do wykonania. Strumieniuje zdarzenia procesów z powrotem tym samym kanałem RPC i nie przechowuje żadnego trwałego stanu: żadnej bazy danych, żadnych poświadczeń, zgód ani budżetów — te nigdy nie opuszczają cc_server. Worker jest kończyną, nie drugim mózgiem. Jeden autorytatywny serwer, N głupich kończyn; nie ma konsensusu ani ruchu między workerami.

Wydzierżawione wykonanie to nadal tylko mechanika dzierżawy. agentRun to jedyny rodzaj, który uruchamia prawdziwe polecenie (CC_JOB_COMMAND albo powtórzony prompt, jeśli żadnego nie podano). pipelineStep, codeIndex, goldenRender, benchmark i evalBatch na razie sondują poleceniem git --version / rev-parse HEAD, aby przetestować ścieżkę strumieniowania, nie udając, że te zadania naprawdę się wykonały.

Ogłasza system operacyjny, architekturę procesora, liczbę rdzeni, pamięć RAM, dostępność pakietu Flutter SDK oraz backendy sandboxa dostępne na hoście (w Windows żaden). Dedykowany worker zawsze ogłasza też alwaysOn i acceptsParallel, pod które planuje harmonogram przypnij → preferuj → przelej. Nie ma sondy GPU ani ML.

Samotny komputer desktopowy nic z tego nie zauważa, bo zwykłe wysyłanie zadań w ogóle nie przechodzi przez flotę. Istnieje wprawdzie niejawny worker działający w tym samym procesie, zarejestrowany dla hosta serwera, ale to jedynie szew rejestracyjny, do którego nie podłączono żadnych modułów wykonawczych — zadanie faktycznie umieszczone na lokalnym workerze kończy się niepowodzeniem, zamiast się wykonać. Dziś tylko prawdziwy cc_worker wykonuje wydzierżawione zadania.

Zagadnienie Serwer (cc_server) Worker floty (cc_worker) Klient desktopowy Klient webowy / na telefonie
Bazy danych (Drift/SQLite) zarządza nimi — — —
Uwierzytelnianie, zgody, budżety, tożsamość zarządza nimi — — —
Potoki, rekonsyliatory, nasłuch orkiestracji uruchamia je — — —
Zestaw narzędzi MCP obsługuje go — — —
Wykonywanie agentów uruchamia je (lub wydzierżawia) wykonuje wydzierżawione zadania — —
Renderowanie interfejsu — — pełne pełne (web) / głównie odczyt (telefon)
Nagrywanie spotkań (mikrofon + dźwięk spotkania) — — natywne nagrywanie tylko web, przez dźwięk z udostępniania ekranu
OAuth kalendarza (Google) przechowuje tokeny — — —

Oba poziomy nie stosują takiego samego sandboxa. cc_server zamyka uruchomienia, które wykonuje samodzielnie, w sandboxie systemu operacyjnego hosta, gdy backend jest dostępny — Seatbelt w macOS, bubblewrap w Linuksie i WSL2 — a w Windows lub bez bwrap i socat sięga po pozostałe ograniczenia. cc_worker w ogóle nie stosuje sandboxa. Ogłasza możliwość sandbox, aby serwer mógł rozdzielać zadania według platformy, ale nic w workerze jej nie egzekwuje: wydzierżawione zadanie jest uruchamiane bezpośrednio, ograniczone środowiskiem dzierżawy i przydzielonym worktree. Wydzierżawianie pracy przenosi więc wykonanie poza sandbox, nawet na hoście, który go ma. Co ogranicza uruchomienie w każdym z tych przypadków, opisano w sekcji Sandbox i bezpieczeństwo.

Nagrywanie spotkań odbywa się po stronie klienta, bo wymaga mikrofonu i dźwięku spotkania — serwer stojący w szafie nie nagra Twojej rozmowy. Nagranie jest następnie podsumowywane przez potok serwera, a notatki trafiają tam, skąd każdy klient może je odczytać.

Klient webowy też nagrywa, co układ tabeli może przesłaniać: przechwytuje w przeglądarce mikrofon oraz ścieżkę dźwięku z udostępniania ekranu i strumieniuje PCM 16 kHz do serwera. Ta ścieżka dźwięku jest wymagana, więc gdy przeglądarka jej nie udostępni, nagrywanie jawnie zawodzi. Wyłącznie desktopowa jest natomiast warstwa jakości wokół nagrywania — anulowanie echa na poziomie sygnału przez natywny procesor, miernik poziomu wejścia i ostrzeżenie o niedziałającym mikrofonie, ponowne generowanie podsumowania, anulowanie przetwarzania oraz akcja „Nagraj i podlinkuj” w kalendarzu.

Każdy klient potwierdza swoją tożsamość wstępnie współdzielonym kluczem, który nadaje się jednorazowo:

  • Aplikacja desktopowa w trybie lokalnym generuje świeży klucz przy każdym starcie i przekazuje go uruchomionemu serwerowi przez zmienne środowiskowe, więc na komputerze nie zapisuje się żaden sekret.
  • Klienci zdalni (desktopowy w trybie zdalnym, webowy, telefon) używają klucza utworzonego przez polecenie pair serwera i przechowywanego w pęku kluczy systemu operacyjnego (a na telefonie — lokalnie).

Unieważnianie działa na bieżąco, a nie leniwie. Serwer obserwuje tabelę sparowanych urządzeń i w ciągu kilku sekund zrywa każdą otwartą sesję, której urządzenie opuściło aktywny zbiór — zarówno na bezpośredniej ścieżce WebSocket, jak i przez przekaźnik. Nie trzeba czekać, aż urządzenie połączy się ponownie, żeby je odciąć.

Klucz nigdy nie podróżuje w adresie URL, który może zobaczyć statyczny host. Gdy trafia przez link — na telefonie lub w przeglądarce — jedzie we fragmencie URL, czyli części po #, której przeglądarka nigdy nie wysyła do serwera hostującego stronę. To niezmiennik wart zapamiętania i dotyczy obu przypadków. Klient webowy ma dwa inne punkty wejścia: formularz połączenia, w którym wkleja się serwer i klucz bezpośrednio, oraz kod zaproszenia, wymieniany na poświadczenie utworzone wyłącznie dla tej przeglądarki.