Sari la conținut

Guardrail-uri — o politică pentru fiecare acțiune care modifică starea

Agenții din Control Center pot șterge fișiere, pot face push pe ramuri, pot publica pull request-uri și pot apela API-uri externe. Fiecare dintre acestea este un alt tip de risc, iar multă vreme doar unul dintre ele — comenzile shell — avea o politică. Guardrail-urile există ca să răspundă uniform la o singură întrebare, pentru fiecare acțiune care modifică starea pe care o poate face un agent, indiferent de runtime-ul sau instrumentul care o propune: este permisă, are nevoie de o aprobare sau este refuzată?

Există exact un motor de politică. El este consultat în fiecare punct în care o acțiune poate ieși din sistem: registrul de instrumente al harness-ului încorporat, poarta dispecerului MCP de pe server și mutațiile operațiunilor pe repo. O regulă scrisă o dată se aplică deci atât runtime-ului încorporat, cât și CLI-urilor de agent externe — nu există o a doua cale, mai tăcută, pe lângă ea. Politica mai veche, doar pentru comenzi bash, încă funcționează: o regulă poate potrivi un prefix de comandă în loc de o clasă de acțiune, iar cele două forme coexistă în același depozit.

Motorul nu este însă un substitut pentru sandbox-ul sistemului de operare, iar sandbox-ul nu este un substitut pentru el. Ele răspund la întrebări diferite: guardrail-urile decid dacă un apel pe care Control Center îl poate vedea are voie să se întâmple, în timp ce sandbox-ul limitează unde poate ajunge procesul rezultat. cc_server aplică sandbox-ul acolo unde gazda oferă un backend — dar Windows nu oferă niciunul, iar Linux are nevoie de bwrap și socat. Pe acele gazde, guardrail-urile sunt stratul de aplicare cel mai exterior, nu o politică așezată deasupra unui prag, ceea ce merită știut când decizi cât de permisiv să fii.

Fiecare instrument care modifică starea declară căror ActionClasses aparține, dintr-un set închis de treisprezece clase de efect. Cinci dintre ele întreabă întâi din start; restul permit, iar o regulă este ce schimbă asta.

Clasă Implicit încorporat Ce acoperă
fileDelete întreabă întâi Ștergerea unui fișier (o scriere de tip ștergere, rm)
gitPush întreabă întâi Push către un remote git
prCreate întreabă întâi Crearea unui pull request
prPublish întreabă întâi Publicarea unei revizuiri, îmbinarea unui PR
vendorSyncWrite întreabă întâi Scrierea către un furnizor extern de tichete (Linear, Jira, sincronizare GitHub)
fileWriteOutsideWorktree permite Scrierea unui fișier în afara worktree-ului izolat
gitCommit permite Crearea unui commit git
networkEgress permite Ieșire în rețea (preluare web, HTTP arbitrar)
secretAccess permite Citirea unui secret sau a unei acreditări
packageInstall permite Instalarea unui pachet (npm, pip, pub, brew)
processSpawn permite Pornirea unui proces (shell și orice apelează shell-ul)
workspaceMutation permite Modificarea structurii spațiului de lucru (repo-uri, spații, agenți)
enclosureControl permite Conducerea unui mediu izolat (stație): pornirea unei VM, trimiterea de input către ea

Cele opt clase permise implicit au fost alese pornind de la premisa că pragul sandbox-ului se aplică sub ele. Se aplică acolo unde gazda oferă un backend și nu se aplică pe Windows sau pe o mașină Linux căreia îi lipsesc bwrap și socat — așa că, dacă vrei ca fileWriteOutsideWorktree sau packageInstall să fie controlate acolo, este o regulă pe care trebuie să o scrii.

Setul este închis în mod deliberat. Un instrument nou care modifică starea și nu declară nicio clasă pică testul cu clichet, așa că taxonomia poate crește doar printr-o decizie explicită și argumentată — „inflația taxonomiei este moartea acestei funcții”. Treisprezece clase sunt destule ca să exprime politicile pe care oamenii chiar le scriu (niciodată push fără să întrebi, niciodată ștergere în afara unui worktree, permite mereu citirile) și destul de puține ca interfața de setări să rămână comprehensibilă.

O regulă mapează (scope, ActionClass | command prefix) → allow | prompt | deny. Domeniile se imbrică de la cel mai specific la cel mai puțin specific:

spațiu > agent > spațiu de lucru > presetare de mod > implicit încorporat

Rezolvarea este întâi după specificitate: primul domeniu cu o regulă potrivită decide, iar rezolvarea se oprește acolo. Cea mai restrictivă este doar o departajare — între două reguli la fel de specifice din același domeniu și la combinarea mai multor clase pe care le declară o singură acțiune. În interiorul unui domeniu, o regulă de comandă se potrivește pe cel mai lung prefix de comandă. Permiterea nu învinge refuzul nicăieri.

Două decizii au garanții stricte:

  • Întrebări închise la eșec. Un prompt fără niciun aprobator conectat se rezolvă în refuz. O întrebare fără răspuns nu este niciodată un da.
  • Combinare cât mai restrictivă. O acțiune care acoperă mai multe clase le combină cât mai restrictiv; când mai multe clase întreabă, o singură confirmare le enumeră pe toate.

Nu există în mod deliberat nicio zăvorâre pe durata unei ture. Fiecare apel de instrument re-rezolvă politica de la zero, așa că întrebarea repetată pentru aceeași acțiune este limitată de „reține această decizie” al operatorului, nu de ceva legat de tură.

O regulă poate constrânge ARGUMENTELE unei acțiuni, nu doar verbul ei. „Poate face push” și „poate face push către feature/*” sunt afirmații diferite, iar doar a doua este un control — o poartă de capabilitate care autorizează doar verbul este forma din spatele majorității incidentelor publicate cu agenți.

O constrângere poate numi:

  • căi — șabloane glob (repos/**, !**/.env)
  • referințe — referințe git, cu negație ! (['**', '!main'] se citește ca „orice ramură în afară de main”)
  • gazde — destinații de rețea, cu wildcard-uri de subdomeniu (*.internal)
  • comenzi — prefixe de comandă, potrivite pe o limită de cuvânt
  • plafoane — maxCount / maxCents, așa încât „șterge până la 50 de fișiere” devine exprimabil

Trei proprietăți decurg din păstrarea gramaticii închise și fără bucle, și toate trei sunt scopul: fiecare regulă se redă ca o propoziție pe care o poți citi înapoi, fiecare refuz poate numi constrângerea care s-a potrivit, iar întreaga politică poate fi enumerată — poți vedea ce poate face un agent înainte ca el să ruleze.

Trei reguli care contează:

  • O regulă cu constrângeri este mai specifică decât una fără constrângeri în același domeniu, așa că „refuză push către main” învinge „permite push” fără ca cineva să se gândească la ordonare.
  • O regulă restrictivă (refuză / întreabă) se aplică atunci când cererea atinge oricare valoare pe care o numește; una permisivă (permite) se aplică doar atunci când cererea este în întregime în interiorul ei. Citirea ambelor la fel ar lăsa un agent să spele o cale interzisă punând una inofensivă alături.
  • O regulă care numește o fațetă despre care cererea nu spune nimic nici nu se aplică, nici nu se sare peste ea: ea escaladează la o aprobare. Săritul în silențiu ar face fiecare regulă de ramură protejată inertă în fața unui instrument care nu își raportează referința; refuzul în silențiu ar refuza fiecare push pe care extractorul nu îl poate descrie. Fără un aprobator conectat, regula de închidere la eșec existentă transformă acea escaladare într-un refuz.

O regulă commandPrefix se potrivește pe o limită de cuvânt: git push acoperă git push origin main, dar nu git pushx. Ea nu normalizează shell-ul. git push (două spații), cd sub && git push și GIT_DIR=… git push sunt șiruri diferite și niciunul nu se potrivește cu o regulă despre git push.

Aceasta este o limită reală și este declarată, nu mascată: regulile de comandă sunt o comoditate pentru operator, ca să modeleze comportamentul obișnuit al agenților, nu o graniță împotriva unui comportament advers. Granițele sunt clasele de efect (pe care un instrument le declară și nu le poate rescrie), plasa de comenzi a modului de sub depozitul de politici și sandbox-ul sistemului de operare de sub ambele. O regulă de comandă care se rezolvă în allow nu slăbește niciuna dintre ele — plasa modului rulează după aceea, iar deny-ul ei este final.

Răspunsul la o întrebare cu „reține asta” scrie o regulă de politică reală: cu domeniu (spațiu / agent / spațiu de lucru), cu constrângeri pe argumente (aprobarea unui push către feature/login acordă feature/**, nu main) și auto-revocabilă după un TTL explicit. A spune da pentru următoarele câteva ore nu este același lucru cu a-ți rescrie în tăcere politica permanentă, iar regulile pe care le scrie ajung în același depozit cu proveniența remembered și o expirare, așa că pot fi revocate mai devreme.

Fiecare regulă poartă unul:

  • consultativ — permite, dar înregistrează că regula s-a potrivit. Aceasta este calea de adoptare: lansează o regulă strictă în mod consultativ, citește urma de audit o săptămână ca să vezi exact ce ar fi blocat, apoi promoveaz-o. Fără acest nivel, politicile stricte nu sunt niciodată activate.
  • flexibil — refuză, cu posibilitate de suprascriere de către cineva care deține permisiunea de suprascriere, cu o justificare înregistrată. Suprascrierea este ea însăși un eveniment auditat.
  • strict (implicit) — refuză. Nimic nu îl suprascrie.

Un operator de instalare poate fixa politici pe care niciun administrator de spațiu de lucru nu le poate slăbi. Regulile administrative trăiesc pe server și sunt îmbinate cât mai restrictiv cu lanțul propriu al fiecărui spațiu de lucru — ele nu sunt un domeniu în fruntea lui, pentru că o regulă administrativă de tip allow aflată în fruntea lanțului ar lăsa instalarea să încalce refuzul unui spațiu de lucru, opusul scopului unui limitator. O regulă administrativă nu poate deci decât să strângă.

Setarea CC_SERVER_MANAGED_POLICY către un fișier JSON are prioritate absolută în fața regulilor stocate, ceea ce permite unui operator să fixeze o postură pe care nicio interfață de administrare nu o poate răsturna — răspunsul la „pot să-mi opresc dezvoltatorii să dezactiveze controalele de siguranță?“.

Cadranul de autonomie este un profil, nu un sistem paralel

Secțiune intitulată „Cadranul de autonomie este un profil, nu un sistem paralel”

Cadranul de autonomie pe spațiu este un profil numit peste același depozit de politici, nu un al doilea mecanism. Cele trei setări ale sale sunt stocate și trimise ca proposeOnly, actWithApproval și actFreely (formele cu cratime sunt doar text de interfață, iar autonomy.setForSpace respinge orice altceva). Dacă îl lași nesetat, se comportă ca actWithApproval.

Ce face fiecare unei decizii rezolvate:

  • proposeOnly refuză direct fiecare instrument controlat. Agentul poate încă să raționeze și să răspundă; pur și simplu nu poate acționa, iar mesajul lui spune de ce.
  • actWithApproval (și nesetat) este poarta de aprobare cu închidere la eșec descrisă mai sus.
  • actFreely pre-aprobă orice nu s-a rezolvat într-un deny strict.

Ultimul merită spus pe față, nu înmuiat: sub actFreely, o decizie prompt nu este escaladată către un aprobator — este permisă și nu este întrebat nimeni. Doar o regulă deny explicită supraviețuiește cadranului. Așadar actFreely este o acordare deliberată de autonomie, nu o setare de confort, iar regula de închidere la eșec protejează exact cazul în care contează (actWithApproval, implicitul).

Delegarea între agenți este păzită la punctul de control delegate_task de toate cele patru garduri: un plafon de adâncime (implicit 3), detectarea ciclurilor, un plafon de autonomie (un delegat nu poate acționa niciodată cu mai multă autonomie decât delegantul lui — privilegiul nu poate fi spălat prin înmânarea muncii către un agent mai liber) și o anvelopă bugetară (delegarea facturează bugetul rămas al delegantului și nu poate crea mai mult). Fiecare este refuzat cu zgomot, cu motivul gardului returnat agentului textual.

Nu orice runtime îi permite Control Center să intercepteze fiecare acțiune. CLI-urile de agent externe rulează unele instrumente nativ, în proces, unde nicio poartă nu poate sta. În loc să pretindă altfel, fiecare adaptor poartă o matrice de onestitate: pe adaptor, dacă Control Center filtrează suprafața de instrumente, interceptează apelurile de instrumente, observă contractul de finalizare, poate vedea instrumentele native ale runner-ului și dacă instrumentele din proces sunt acoperite de un profil de sandbox.

Două intrări merită citite înainte să ai încredere într-o configurație. Harness-ul încorporat declară că instrumentele lui de fișiere din proces nu sunt în sandbox — suprafața de instrumente și acest motor de politică sunt singura graniță de sistem de fișiere pe care o are. Claude Code declară că propriile apeluri de instrumente nu sunt interceptabile, pentru că este lansat cu --dangerously-skip-permissions; Control Center vede apelurile lui mcp__* și, printr-un hook, liniile lui de comandă Bash, în timp ce instrumentele lui de citire, scriere și editare rulează nevăzute.

Ecranul de setări arată această matrice, pentru că singurul eșec de neiertat pentru un sistem de guardrail-uri este să pretindă o acoperire care nu există.

Guardrail-urile se află la Setări → Spațiu de lucru → Permisiuni agent (/workspaces/<id>/settings/workspace/permissions). Ecranul are matricea de politici unde se scriu regulile, șabloane de politică, o sondă de tip „ce-ar fi dacă”, urma de audit a autorizării și matricea de onestitate a adaptoarelor.

Sonda primește o clasă de acțiune (sau o comandă) și un domeniu și arată ce regulă a decis și de ce. Pentru că rezolvarea este pură și deterministă — aceeași acțiune, același domeniu, aceeași decizie — răspunsul sondei este răspunsul pe care îl va primi agentul.