Sari la conținut

Sandbox și securitate

Un agent citește fișiere, rulează comenzi shell și apelează API-uri externe în numele tău. Eșecul care te îngrijorează este rareori rea-voință — este un agent care citește greșit o cale și îți rescrie checkout-ul de lucru, sau unul care împinge o ramură pe care nu terminai de revizuit. Așa că întrebarea la care trebuie să răspundă produsul nu este „este agentul demn de încredere”, ci „care este raza de impact atunci când greșește”.

Control Center răspunde la asta pe straturi, iar straturile sunt independente intenționat. Dacă lipsește unul, celelalte rezistă în continuare. Asta contează, pentru că stratul cel mai exterior — sandbox-ul de sistem — este singurul condiționat: depinde de faptul că gazda oferă un backend, iar pe Windows niciuna nu o face.

Cinci lucruri mărginesc o rulare de agent pe orice gazdă, cu sau fără sandbox. Niciunul dintre ele nu este o graniță de kernel; toate sunt reale. Acolo unde gazda oferă un backend, sandbox-ul de sistem stă deasupra lor, iar secțiunea „Ce adaugă sandbox-ul și unde se oprește” de mai jos acoperă ce schimbă asta.

Propriul worktree copy-on-write. Fiecare conversație își provisionează propriul checkout pentru fiecare repo legat, clonat copy-on-write prin nativul rift inclus, nu copiat octet cu octet, așa că izolarea este suficient de ieftină încât să fie varianta implicită. Repository-ul înregistrat pe care l-ai adăugat în spațiul de lucru nu este directorul în care lucrează agentul — două conversații care ating același repo nu pot vedea modificările celeilalte, iar niciuna nu-ți editează checkout-ul.

Granularitatea merită precizată: worktree-ul este per conversație, nu per agent. Doi agenți din aceeași conversație primesc directoare de lucru overlay distincte care împart directorul repos/ al acelei conversații — ceea ce este și ideea, fiindcă ei colaborează la o singură modificare — în timp ce directoarele lor private de agent rămân separate. Copy-on-write este singurul backend oriunde este livrat rift: un sistem de fișiere care nu poate face reflink, sau un director de date aflat pe alt volum decât repo-ul, face ca provisionarea să eșueze, în loc să revină la altă variantă. Windows este excepția — acolo nu există niciun backend copy-on-write, așa că git worktree este backendul, nu o degradare silențioasă.

Confinarea căilor în instrumentele de fișiere ale harness-ului încorporat. Instrumentele read, write, edit, apply_patch, search, find și search_files ale harness-ului rezolvă fiecare cale față de directorul de lucru al rulării plus rădăcinile partajate — directorul repos/ al conversației, unde se află worktree-urile, și directorul de atașamente al spațiului, când există — și refuză orice iese în afara lor. Căile absolute sunt acceptate doar dacă erau deja înăuntru. Un symlink aflat în interiorul worktree-ului care indică spre exterior este rezolvat și refuzat la fel. De aceea un agent nu poate scrie cu write în ~/.ssh nici atunci când sandbox-ul nu intră deloc în discuție.

Suprafața de instrumente a modului. Modul unei conversații decide ce instrumente sunt materializate pentru rulare. În modurile doar pentru citire (plan, review, orchestrate) instrumentele care modifică starea pur și simplu nu se află în registru, iar politica de comenzi shell a acestor moduri refuză categoric fiecare comandă care modifică starea — git commit, git push, npm install, docker și restul — în loc să ceară aprobarea pentru ele. Un instrument care nu este prezent nu poate fi apelat, ceea ce este o garanție mai puternică decât un instrument prezent și filtrat.

Guardrail-urile unificate de acțiuni. Fiecare instrument care modifică starea declară clasele de efect pe care le poate produce, iar motorul de politici rezolvă fiecare dintre ele la permite, cere aprobare sau refuză înainte ca apelul să ruleze. Un prompt fără niciun aprobator conectat se rezolvă la refuzat — o întrebare la care nimeni nu poate răspunde nu este niciodată un da. Vezi Guardrail-uri pentru modelul de rezolvare.

Date de autentificare emise de broker și un mediu sanitizat. Datele de autentificare sunt emise la fiecare lansare și sunt revocate când rularea se desființează, iar tokenul GitHub pe care îl are o rulare poate citi, dar nu poate face push — vezi „Push-urile trec prin Control Center” mai jos. ACP, bash-ul harness-ului și Claude Code sub învelișul nativ primesc un mediu curățat de vectorii de injecție prin linker dinamic și interpretor (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND și rudele lor) înainte ca procesul să pornească. Claude Code prin NoSandboxAdapter moștenește mediul gazdei.

Instrumentele de fișiere ale harness-ului rulează în procesul serverului, nu într-un proces copil, așa că niciun profil de sandbox nu le-ar putea limita chiar dacă ar fi atașat unul — doar bash trece prin executorul de comenzi sandboxat. Confinarea lor este verificarea de căi descrisă mai sus, iar acea verificare este prin urmare esențială, nu o a doua linie de apărare.

Instrumentul bash este celălalt loc unde trebuie să fim precisi. Comenzile lui sunt evaluate față de politica de comenzi a modului și apoi lansate cu un mediu sanitizat și cu worktree-ul ca director de lucru. Acolo unde există un backend, acea lansare este învelită, așa că sandbox-ul este ceea ce oprește o comandă shell să scrie în afara worktree-ului; acolo unde nu există niciunul — Windows, sau Linux fără bwrap și socat — politica de comenzi este singurul lucru dintre comandă și restul discului. În modul chat, politica refuză categoric o listă fixă (escaladarea privilegiilor, distrugerea sistemului și a discului, evadarea din container și din namespace) și cere aprobare pentru mutațiile stării de la distanță (git push, gh pr create, publicări și instalări de pachete). Orice nu se află pe niciuna dintre liste rulează. Pipeline-urile, lanțurile și subshell-urile sunt analizate, iar fiecare subcomandă este evaluată separat, câștigând decizia cea mai restrictivă.

Claude Code, când este folosit ca adaptor, este cazul în care sandbox-ul are cea mai mare greutate. Control Center îl lansează cu --dangerously-skip-permissions, pentru că un claude -p neinteractiv s-ar bloca altfel la nesfârșit pe propriul prompt de aprobare. Apelurile sale de citire, scriere și editare au loc în interiorul propriului proces, iar Control Center nu le vede niciodată. Apelurile sale Bash sunt trimise, înainte să ruleze, unui hook de pe server care clasifică linia de comandă și întreabă regula corespunzătoare, iar instrumentele mcp__* pe care le deservește Control Center trec prin poartă ca de obicei. Acolo unde există un backend, acele apeluri nevăzute au loc totuși în interiorul învelișului de sandbox, așa că sistemul de operare este granița din jurul lor. Acolo unde nu există niciunul, nimic nu este. Fiecare adaptor declară asta în matricea sa de aplicare — vezi matricea de onestitate a adaptoarelor.

Nu există indicatori de permisiune per agent. Dacă un agent poate crea un commit, face push, deschide un pull request, scrie într-un tracker sau ajunge la rețea decid guardrail-urile — permite, întreabă întâi sau refuză — iar aceleași reguli se aplică indiferent de adaptorul care rulează agentul.

Un push este acțiunea care trebuie să reziste chiar și în fața unui agent care încearcă să ocolească o regulă, așa că este impus prin ce deține agentul, nu prin ce rulează. Cu un GitHub App configurat, o rulare într-un worktree GitHub primește un token de instalare limitat la depozit, care poate citi conținut și lucra cu pull request-uri, dar nu poate face push. Configurația git a rulării rescrie fiecare URL de push GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) către gateway-ul rulărilor de agenți, un punct final de loopback pe cc_server. Gateway-ul citește ce ref-uri actualizează push-ul, întreabă regula Push către un remote — așa că se aplică și o regulă limitată la ramuri, precum „refuză push-ul pe main” — și abia apoi transmite push-ul către GitHub cu un token de scriere pe care îl emite în acest scop și nu îl dă niciodată agentului. Un push refuzat apare chiar în ieșirea git:

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

Gateway-ul transmite push-uri doar către depozitul propriu al worktree-ului sau către un depozit GitHub legat de spațiul de lucru. Fetch-urile merg direct la GitHub cu tokenul de citire.

Celelalte comenzi de shell întâlnesc o poartă mai blândă. Instrumentul bash al harness-ului și, printr-un hook, instrumentul Bash al Claude Code clasifică fiecare linie de comandă și întreabă regula în care se încadrează: git commit, merge, rebase, cherry-pick, revert și am sunt Creează un commit; gh pr create este Deschide un pull request; gh pr merge, review, comment și close sunt Publică o revizuire sau o fuziune; gh issue create, edit, comment și close sunt Scrie într-un tracker extern; instalările de pachete sunt Instalează un pachet; curl și wget sunt Accesează rețeaua. Asta recunoaște forma obișnuită de a scrie o acțiune, iar un agent hotărât să o ascundă (eval, un fișier de script, un apel direct la API) poate face asta. Push-urile nu depind de asta.

Nicio cheie API de tichete nu este pusă într-un sandbox. Agenții scriu tichete prin instrumentele MCP ale Control Center, care întreabă Scrie într-un tracker extern.

Regula Accesează rețeaua este citită o singură dată, când pornește rularea. Rețeaua unui sandbox este fie pornită, fie oprită, așa că refuzul o oprește, iar permite și întreabă întâi o lasă pornită; instrumentele web_fetch și web_search ale harness-ului întreabă în continuare la fiecare apel. Datele de autentificare sunt revocate când rularea se desființează, împreună cu orice token de scriere emis de gateway, așa că un handle nu supraviețuiește rulării.

Enumerația SandboxBackend are trei valori — native (Seatbelt pe macOS, bubblewrap pe Linux și WSL2), none și microvm. Învelirea comenzilor agentului folosește doar primele două: microvm este backendul de VM izolat pentru terminalele interactive și stații, nu o upgradare silențioasă a unui înveliș bash. Înțelegerea formei native explică ce câștigi acolo unde este atașat unul și ce nu ți-ar fi dat niciodată.

cc_server sondează gazda o dată la pornire, iar acolo unde un backend răspunde hrănește ambele cusături pe care le are calea de trimitere, pentru că transporturile nu împart una singură. Prima (SandboxPort) învelește CLI-ul Claude Code. A doua (SandboxManager) învelește adaptoarele ACP și instrumentul bash al harness-ului încorporat. Dacă ai hrăni doar una, celelalte ar fi lansate neînvelite, ceea ce se și întâmpla înainte. Pe o gazdă fără backend, sau sub --sandbox off, toate sunt lansate neînvelite, iar serverul spune în ce caz te afli pe linia sa de pornire. ACP și bash-ul harness-ului sanitizează în continuare mediul; Claude Code prin NoSandboxAdapter moștenește mediul gazdei.

O suprafață se sustrage în mod deliberat: terminalul găzduit de server acceptă un argument de backend per sesiune, iar cc_server îl construiește implicit cu shell-ul gazdei, așa că un terminal pe care îl deschizi din aplicație rulează fără sandbox dacă un client nu numește explicit native. Acela este un shell real pe gazda serverului, nu o rulare de agent.

Pe Linux, bubblewrap construiește un namespace de montare, așa că agentul vede într-adevăr doar ce a fost legat în el. Pe macOS, sandbox-exec aplică un profil Seatbelt care este permisiv implicit — un profil complet cu refuz implicit face un shell interactiv inutilizabil, pentru că numărul de apeluri dyld, XPC și Mach de enumerat este nepractic. Profilul macOS pornește deci de la (allow default) și decupează interdicții: citiri ale căilor secrete, o politică de scriere resetată la refuz cu permisiuni explicite deasupra, blocarea ștergerii (unlink) pe fiecare cale interzisă și pe strămoșii ei, astfel încât o redenumire să nu poată ocoli o interdicție de scriere, interdicții de execuție pentru binarele mereu periculoase și pentru orice este copiat într-un director inscriptibil, și rețeaua restricționată la porturile proxy de loopback plus DNS.

Consecința practică: pe Linux, vederea agentului asupra sistemului de fișiere este îngustată; pe macOS, agentul poate în continuare citi cea mai mare parte a gazdei, iar checkout-urile tale înregistrate sunt protejate prin interdicții explicite de scriere și de redenumire, nu prin invizibilitate. Ambele sunt izolare la nivel de namespace, nu o graniță de securitate a kernelului — backend-urile spun asta în propriile note de capabilități.

Setul de montări bind este locul unde este trasată granița dintre agenți. Trimiterea pe overlay (Claude Code și ACP) montează până la patru căi, rezolvate per rulare: directorul de lucru overlay al agentului, cu citire-scriere, directorul de configurare al agentului, cu citire doar, directorul partajat repos/ al conversației, cu citire-scriere când există, și directorul de atașamente al spațiului, cu citire doar când există. Directorul unui agent frate nu este niciodată montat. Învelișul bash al harness-ului este mai îngust: montează prin bind doar directorul de lucru al rulării. Aceasta este aceeași formă pe care o impune în software verificarea de căi în proces a harness-ului, așa că cele două sunt de acord — dar doar verificarea de căi acoperă instrumentele de fișiere proprii ale harness-ului, care nu ajung niciodată la un namespace de montare.

Checkout-urile tale înregistrate se alătură setului cu scriere interzisă peste asta, în fiecare mod și pe toate cele trei căi învelite (Claude Code, ACP, bash-ul harness-ului), iar fiecare regulă este emisă atât în grafia ei literală, cât și în cea rezolvată prin symlink. A doua grafie nu este un moft: kernelul potrivește o regulă de interdicție cu calea rezolvată, iar macOS face din rezolvare cazul obișnuit, nu cel exotic — /tmp este de fapt /private/tmp, /var/folders/… este de fapt /private/var/folders/… — așa că un checkout accesat prin oricare dintre ele ar fi fost inscriptibil în timp ce profilul părea să îl interzică.

Rețeaua, sub un sandbox, este o listă de permisiuni pe domenii, per sesiune, impusă de proxy-uri de loopback în proces. Ieșirea în rețea este refuzată implicit: cu rețeaua oprită, proxy-urile pornesc totuși, dar lista lor de permisiuni este goală, așa că fiecare conexiune de ieșire este refuzată. Cu ea pornită, rezolvatorul construiește lista de permisiuni dintr-o bază de referință atent selectată — furnizori de API pentru LLM-uri, găzduire git, registre de pachete, catalogul de modele — plus domeniile GitHub, și potrivește gazda țintă a fiecărei conexiuni cu ea. Wildcard-urile funcționează (*.example.com potrivește subdomeniile, nu apexul). O listă separată, mereu interzisă, are prioritate în fața listei de permisiuni și acoperă punctele finale de metadate din cloud și destinațiile de telemetrie. Ceea ce nu este configurabil este lista în sine: specificația poartă un câmp egressAllowlist pentru domenii suplimentare, dar nimic din calea de trimitere nu îl populează, așa că în practică fiecare rulare primește baza de referință plus GitHub, și nimic mai mult.

Interdicțiile, pe macOS, sunt observabile: un monitor interceptează jurnalul de sistem pentru interdicțiile Sandbox, filtrează zgomotul cunoscut, elimină duplicatele furtunilor de reîncercări și expune fiecare interdicție ca un SandboxViolation care numește ce s-a încercat și ce a fost blocat. Pe Linux, calea de învelire analizează stderr după EPERM, Operation not permitted și Permission denied și emite același tip de eveniment — nu există nicio interceptare a jurnalului de sistem. Acestea ajung pe fluxul propriu de evenimente al rulării, așa că un apel de sistem refuzat apare în transcriere, nu doar într-un jurnal. Telemetria există doar cât timp un sandbox nativ învelește efectiv procesul — pe o gazdă fără backend nu există nimic de observat. De asemenea, nu are legătură cu guardrail-urile: o încălcare provine dintr-un apel de sistem refuzat, niciodată dintr-o căutare de regulă.

Sandbox-ul nu are nicio cale de aprobare. El lansează, execută, emite evenimente și se desființează; nu există în el niciun loc în care un om să răspundă la o întrebare. Când vezi o confirmare într-un spațiu — aprobă acest push, aprobă această ștergere — ea a fost ridicată de motorul de guardrail-uri (sau de politica de comenzi a harness-ului) înainte ca acțiunea să ajungă deloc la runtime.

Această ordine este deliberată. O aprobare care s-ar produce după apelul de sistem ar fi o înregistrare de audit, nu o poartă. De aceea regula cu eșec închis stă în guardrail-uri: fără niciun aprobator conectat, decizia este luată înainte ca orice să ruleze și este un refuz.

Diagnostichează un agent este singurul loc care declară ce poate fi verificat manual.