Przejdź do głównej zawartości

Sandbox i bezpieczeństwo

Agent czyta pliki, uruchamia polecenia powłoki i wywołuje zewnętrzne API w Twoim imieniu. Awaria, która Cię interesuje, rzadko wynika ze złej woli — to raczej agent, który błędnie odczyta ścieżkę i nadpisze Twoją kopię roboczą, albo taki, który wypchnie gałąź, której nie zdążyłeś przejrzeć. Pytanie, na które produkt musi odpowiedzieć, nie brzmi więc „czy agentowi można zaufać”, lecz „jaki jest promień rażenia, gdy się myli”.

Control Center odpowiada na to warstwami, które celowo są niezależne. Jeśli jednej brakuje, pozostałe nadal działają. Ma to znaczenie, bo najbardziej zewnętrzna warstwa — sandbox systemowy — jest jedyną warunkową: zależy od tego, czy host oferuje backend, a w Windows żaden host go nie oferuje.

Na każdym hoście — z sandboxem czy bez — uruchomienie agenta ogranicza pięć rzeczy. Żadna z nich nie jest granicą jądra; wszystkie są realne. Tam, gdzie host oferuje backend, sandbox systemowy sytuuje się ponad nimi, a sekcja „Co dodaje sandbox i gdzie się zatrzymuje” poniżej opisuje, co to zmienia.

Własny worktree copy-on-write. Każda rozmowa przygotowuje własną kopię roboczą każdego powiązanego repozytorium, klonowaną copy-on-write przez dołączony natywny komponent rift, a nie kopiowaną bajt po bajcie, więc izolacja jest wystarczająco tania, by była ustawieniem domyślnym. Zarejestrowane repozytorium, które dodałeś do obszaru roboczego, to nie katalog, w którym pracuje agent — dwie rozmowy sięgające do tego samego repozytorium nie widzą swoich zmian i żadna z nich nie edytuje Twojej kopii roboczej.

Warto doprecyzować ziarnistość: worktree przypada na rozmowę, a nie na agenta. Dwaj agenci w tej samej rozmowie dostają odrębne katalogi robocze overlay, które dzielą katalog repos/ tej rozmowy — i o to właśnie chodzi, skoro współpracują nad jedną zmianą — a ich prywatne katalogi agentów pozostają osobne. Copy-on-write jest jedynym backendem wszędzie tam, gdzie trafia rift: system plików, który nie potrafi reflinkować, albo katalog danych na innym wolumenie niż repozytorium, powoduje niepowodzenie aprowizacji, a nie przejście do trybu zapasowego. Wyjątkiem jest Windows — nie ma tam backendu copy-on-write, więc backendem jest git worktree, a nie cicha degradacja.

Izolacja ścieżek w narzędziach plikowych wbudowanego harnessa. Narzędzia read, write, edit, apply_patch, search, find i search_files harnessa rozwiązują każdą ścieżkę względem katalogu roboczego uruchomienia oraz współdzielonych katalogów głównych — katalogu repos/ rozmowy, w którym żyją worktree, i katalogu załączników przestrzeni, jeśli istnieje — i odmawiają dostępu do wszystkiego, co wypada poza nie. Ścieżki bezwzględne są akceptowane tylko wtedy, gdy już znajdowały się w środku. Dowiązanie symboliczne wewnątrz worktree wskazujące na zewnątrz również jest rozwiązywane i odrzucane. Dlatego agent nie może zapisać przez write do ~/.ssh, nawet gdy w ogóle nie ma sandboxa.

Powierzchnia narzędzi trybu. Tryb rozmowy decyduje o tym, które narzędzia w ogóle zostaną zmaterializowane dla danego uruchomienia. W trybach tylko do odczytu (plan, review, orchestrate) narzędzia modyfikujące po prostu nie trafiają do rejestru, a polityka poleceń powłoki dla tych trybów wprost odmawia każdego polecenia modyfikującego — git commit, git push, npm install, docker i reszty — zamiast o nie pytać. Narzędzia, którego nie ma, nie da się wywołać, co jest silniejszą gwarancją niż narzędzie obecne i objęte bramką.

Ujednolicone guardraile działań. Każde narzędzie modyfikujące deklaruje klasy efektów, które może wywołać, a silnik polityk rozstrzyga każdą z nich na zezwolenie, pytanie albo odmowę, zanim wywołanie się wykona. prompt bez podłączonego zatwierdzającego rozstrzyga się na odmowę — pytanie, na które nikt nie może odpowiedzieć, nigdy nie jest zgodą. Model rozstrzygania opisują guardraile.

Poświadczenia z brokera i oczyszczone środowisko. Poświadczenia są tworzone przy każdym uruchomieniu i unieważniane, gdy uruchomienie się kończy, a token GitHub, który ma uruchomienie, pozwala czytać, ale nie wykonać push — zobacz „Push przechodzi przez Control Center” poniżej. ACP, bash harnessa i Claude Code w natywnym opakowaniu dostają środowisko pozbawione wektorów wstrzykiwania przez konsolidator dynamiczny i interpreter (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND i im podobnych), zanim proces wystartuje. Claude Code przez NoSandboxAdapter dziedziczy środowisko hosta.

Narzędzia plikowe harnessa działają w procesie serwera, a nie w procesie potomnym, więc żaden profil sandboxa nie mógłby ich ograniczyć, nawet gdyby był podłączony — przez sandboxowany runner poleceń przechodzi tylko bash. Ich izolację zapewnia opisane wyżej sprawdzanie ścieżek, które jest więc elementem nośnym, a nie drugą linią obrony.

bash to drugie miejsce, które warto doprecyzować. Jego polecenia są oceniane względem polityki poleceń danego trybu, a następnie uruchamiane z oczyszczonym środowiskiem i worktree jako katalogiem roboczym. Tam, gdzie istnieje backend, to uruchomienie jest opakowane, więc to sandbox powstrzymuje polecenie powłoki przed zapisem poza worktree; tam, gdzie go nie ma — Windows albo Linux bez bwrap i socat — polityka poleceń jest jedyną rzeczą między poleceniem a resztą dysku. W trybie chat polityka twardo odmawia w przypadku stałej listy (podnoszenie uprawnień, niszczenie systemu i dysku, ucieczka z kontenera i przestrzeni nazw) i pyta o mutacje stanu zdalnego (git push, gh pr create, publikowanie i instalowanie pakietów). Wszystko, czego nie ma na żadnej z list, zostaje uruchomione. Potoki, łańcuchy i podpowłoki są parsowane, a każde podpolecenie oceniane osobno, przy czym wygrywa najbardziej restrykcyjna decyzja.

Claude Code użyty jako adapter to przypadek, w którym sandbox ma największe znaczenie. Control Center uruchamia go z --dangerously-skip-permissions, ponieważ nieinteraktywne claude -p w przeciwnym razie zablokowałoby się na zawsze na własnym pytaniu o zatwierdzenie. Jego wywołania odczytu, zapisu i edycji odbywają się wewnątrz własnego procesu i Control Center ich nie widzi. Jego wywołania Bash trafiają przed wykonaniem do hooka na serwerze, który klasyfikuje wiersz poleceń i pyta odpowiednią regułę, a narzędzia mcp__* udostępniane przez Control Center przechodzą przez bramkę jak zwykle. Tam, gdzie istnieje backend, te niewidoczne wywołania i tak odbywają się w opakowaniu sandboxa, więc granicą wokół nich jest system operacyjny. Tam, gdzie go nie ma, nie ma żadnej. Każdy adapter stwierdza to w swojej macierzy egzekwowania — zobacz macierz uczciwości adapterów.

Nie ma flag uprawnień dla poszczególnych agentów. O tym, czy agent może utworzyć commit, wykonać push, otworzyć pull request, zapisywać do trackera albo sięgać do sieci, decydują guardraile — zezwól, zapytaj najpierw lub odmów — i te same reguły obowiązują niezależnie od adaptera, który uruchamia agenta.

Push to akcja, która musi się utrzymać nawet wobec agenta próbującego obejść regułę, więc egzekwuje się ją przez to, co agent posiada, a nie przez to, co uruchamia. Przy skonfigurowanej GitHub App uruchomienie w worktree GitHuba dostaje token instalacji o zakresie repozytorium, który może czytać zawartość i pracować z pull requestami, ale nie może wykonać push. Konfiguracja gita uruchomienia przepisuje każdy adres URL push do GitHuba (https://github.com/, git@github.com:, ssh://git@github.com/) na bramę uruchomień agentów, punkt końcowy na pętli zwrotnej w cc_server. Brama odczytuje, które refy push aktualizuje, pyta regułę Push do zdalnego repozytorium — więc obowiązuje też reguła ograniczona do gałęzi, na przykład „odmów push do main” — i dopiero wtedy przekazuje push do GitHuba z tokenem zapisu, który tworzy w tym celu i nigdy nie przekazuje agentowi. Odrzucony push widać w wyjściu samego gita:

! [remote rejected] main -> main (Control Center policy: …)

Brama przekazuje push tylko do własnego repozytorium worktree albo do repozytorium GitHuba połączonego z obszarem roboczym. Fetch trafia bezpośrednio do GitHuba z tokenem odczytu.

Inne polecenia powłoki napotykają łagodniejszą bramkę. Narzędzie bash harnessa oraz, przez hook, narzędzie Bash Claude Code klasyfikują każdy wiersz poleceń i pytają regułę, pod którą podpada: git commit, merge, rebase, cherry-pick, revert i am to Utworzenie commita; gh pr create to Otwarcie pull requesta; gh pr merge, review, comment i close to Opublikowanie recenzji lub scalenie; gh issue create, edit, comment i close to Zapis do zewnętrznego trackera; instalacje pakietów to Instalacja pakietu; curl i wget to Dostęp do sieci. To rozpoznaje typowy zapis akcji, a agent zdecydowany ją ukryć (eval, plik skryptu, bezpośrednie wywołanie API) jest w stanie to zrobić. Push od tego nie zależy.

Do sandboxa nie trafia żaden klucz API zgłoszeń. Agenci zapisują zgłoszenia przez narzędzia MCP Control Center, które pytają regułę Zapis do zewnętrznego trackera.

Reguła Dostęp do sieci jest odczytywana raz, na starcie uruchomienia. Sieć sandboxa jest albo włączona, albo wyłączona, więc odmowa ją wyłącza, a zezwól i zapytaj najpierw zostawiają ją włączoną; narzędzia web_fetch i web_search harnessa nadal pytają przy każdym wywołaniu. Poświadczenia są unieważniane, gdy uruchomienie się kończy, razem z każdym tokenem zapisu utworzonym przez bramę, więc uchwyt nie przeżywa uruchomienia.

Wyliczenie SandboxBackend ma trzy wartości — native (Seatbelt w macOS, bubblewrap w Linuksie i WSL2), none i microvm. Opakowywanie poleceń agenta korzysta tylko z dwóch pierwszych: microvm to backend zamkniętej maszyny wirtualnej dla interaktywnych terminali i rigów, a nie ciche ulepszenie opakowania bash. Zrozumienie kształtu natywnego wyjaśnia, co zyskujesz, gdy jest podłączony, i czego nigdy nie miał ci dać.

cc_server sonduje host raz przy starcie i jeśli backend odpowiada, zasila oba punkty podłączenia, które ma ścieżka wysyłania, bo transporty nie dzielą jednego. Pierwszy (SandboxPort) opakowuje CLI Claude Code. Drugi (SandboxManager) opakowuje adaptery ACP i narzędzie bash wbudowanego harnessa. Zasilenie tylko jednego pozostawiłoby pozostałe uruchamiane bez opakowania — i tak właśnie kiedyś było. Na hoście bez backendu albo przy --sandbox off wszystkie uruchamiają się bez opakowania, a serwer w wierszu startowym mówi, który to przypadek. ACP i bash harnessa nadal oczyszczają środowisko; Claude Code przez NoSandboxAdapter dziedziczy środowisko hosta.

Jedna powierzchnia celowo się wycofuje: terminal hostowany na serwerze przyjmuje argument backendu dla sesji, a cc_server konstruuje go tak, że domyślnie wskazuje powłokę hosta, więc terminal otwarty z aplikacji działa bez sandboxa, chyba że klient wprost wskaże native. To prawdziwa powłoka na hoście serwera, a nie uruchomienie agenta.

W Linuksie bubblewrap buduje przestrzeń nazw montowania, więc agent naprawdę widzi tylko to, co zostało do niej podmontowane. W macOS sandbox-exec stosuje profil Seatbelt, który jest domyślnie permisywny — w pełni odmawiający domyślnie profil czyni interaktywną powłokę bezużyteczną, bo liczba wywołań dyld, XPC i Mach do wyliczenia jest niepraktyczna. Profil macOS wychodzi więc od (allow default) i wycina z niego odmowy: odczyty ścieżek z sekretami, politykę zapisu zresetowaną do odmowy z jawnymi zezwoleniami na wierzchu, blokowanie odłączania (unlink) na każdej zabronionej ścieżce i jej przodkach, tak aby zmiana nazwy nie ominęła zakazu zapisu, odmowy wykonania (exec) dla zawsze niebezpiecznych plików binarnych i dla wszystkiego, co skopiowano do katalogu z prawem zapisu, oraz sieć ograniczoną do portów proxy na pętli zwrotnej plus DNS.

Praktyczna konsekwencja: w Linuksie widok systemu plików dostępny dla agenta jest zawężony; w macOS agent nadal może czytać większość hosta, a Twoje zarejestrowane kopie robocze chronią jawne odmowy zapisu i zmiany nazwy, a nie niewidzialność. Oba przypadki to izolacja na poziomie przestrzeni nazw, a nie granica bezpieczeństwa jądra — same backendy tak o tym mówią w swoich notatkach o możliwościach.

Zbiór podmontowań jest miejscem, w którym wyznacza się granicę między agentami. Wysyłanie przez overlay (Claude Code i ACP) montuje do czterech ścieżek, ustalanych dla każdego uruchomienia: katalog roboczy overlay agenta w trybie odczytu i zapisu, katalog konfiguracji agenta tylko do odczytu, współdzielony katalog repos/ rozmowy w trybie odczytu i zapisu, jeśli istnieje, oraz katalog załączników przestrzeni tylko do odczytu, jeśli istnieje. Katalog bratniego agenta nigdy nie jest montowany. Opakowanie bash harnessa jest węższe: montuje wyłącznie katalog roboczy uruchomienia. To ten sam kształt, który sprawdzanie ścieżek w procesie harnessa wymusza programowo, więc oba są spójne — ale tylko sprawdzanie ścieżek obejmuje własne narzędzia plikowe harnessa, które nigdy nie trafiają do przestrzeni nazw montowania.

Twoje zarejestrowane kopie robocze dołączają ponadto do zbioru objętego zakazem zapisu, w każdym trybie i na wszystkich trzech opakowanych ścieżkach (Claude Code, ACP, bash harnessa), a każda reguła jest emitowana zarówno w zapisie dosłownym, jak i w zapisie po rozwiązaniu dowiązań symbolicznych. Drugi zapis nie jest ozdobnikiem: jądro dopasowuje regułę odmowy do ścieżki po rozwiązaniu, a macOS czyni rozwiązywanie przypadkiem powszechnym, a nie egzotycznym — /tmp to w rzeczywistości /private/tmp, /var/folders/… to w rzeczywistości /private/var/folders/… — więc kopia robocza osiągnięta przez którykolwiek z nich byłaby zapisywalna, choć profil pozornie jej tego zabraniał.

Sieć w sandboxie to lista dozwolonych domen dla sesji, egzekwowana przez działające w procesie proxy na pętli zwrotnej. Ruch wychodzący jest domyślnie blokowany: przy wyłączonej sieci proxy wciąż startują, ale ich lista dozwolonych jest pusta, więc każde połączenie wychodzące jest odrzucane. Przy włączonej resolver buduje listę dozwolonych z wyselekcjonowanej bazy — dostawcy API LLM, hosting gita, rejestry pakietów, katalog modeli — plus domeny GitHuba i dopasowuje do niej host docelowy każdego połączenia. Wieloznaczniki działają (*.example.com dopasowuje subdomeny, a nie samą domenę). Osobna, zawsze zabroniona lista wygrywa z listą dozwolonych i obejmuje punkty końcowe metadanych chmury oraz odbiorniki telemetrii. To, co nie jest konfigurowalne, to sama lista: specyfikacja niesie pole egressAllowlist na dodatkowe domeny, ale nic w ścieżce wysyłania go nie wypełnia, więc w praktyce każde uruchomienie dostaje bazę plus GitHuba i nic więcej.

Odmowy w macOS są obserwowalne: monitor podczepia się do logu systemowego pod kątem odmów Sandbox, filtruje znany szum, deduplikuje burze ponowień i pokazuje każdą jako SandboxViolation, nazywając to, czego próbowano, i to, co zostało zablokowane. W Linuksie ścieżka opakowania parsuje stderr pod kątem EPERM, Operation not permitted i Permission denied i emituje ten sam typ zdarzenia — nie ma podczepienia do logu systemowego. Trafiają one do własnego strumienia zdarzeń uruchomienia, więc odrzucone wywołanie systemowe pojawia się w transkrypcie, a nie tylko w logu. Telemetria istnieje tylko wtedy, gdy natywny sandbox faktycznie opakowuje proces — na hoście bez backendu nie ma czego obserwować. Nie ma też związku z guardrailami: naruszenie pochodzi z odrzuconego wywołania systemowego, nigdy ze sprawdzenia reguły.

Potwierdzenia pochodzą od guardraili, a nie od sandboxa

Dział zatytułowany „Potwierdzenia pochodzą od guardraili, a nie od sandboxa”

Sandbox nie ma ścieżki zatwierdzania. Uruchamia, wykonuje, emituje zdarzenia i się kończy; nie ma w nim miejsca, w którym człowiek mógłby odpowiedzieć na pytanie. Gdy widzisz w przestrzeni potwierdzenie — zatwierdź to wypchnięcie, zatwierdź to usunięcie — zostało ono zgłoszone przez silnik guardraili (albo politykę poleceń harnessa) zanim działanie w ogóle dotarło do środowiska wykonawczego.

Ta kolejność jest celowa. Zgoda wydana po wywołaniu systemowym byłaby zapisem audytowym, a nie bramką. To także dlatego reguła domyślnej odmowy (fail-closed) mieszka w guardrailach: gdy nie ma podłączonego zatwierdzającego, decyzja zapada, zanim cokolwiek się uruchomi, i jest to odmowa.

Uruchom diagnostykę agenta to jedyne miejsce, które opisuje, co można sprawdzić ręcznie.