ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 171
Numărul 170 Numărul 169 Numărul 168 Numărul 167 Numărul 166 Numărul 165 Numărul 164 Numărul 163 Numărul 162 Numărul 161 Numărul 160 Numărul 159 Numărul 158 Numărul 157 Numărul 156 Numărul 155 Numărul 154 Numărul 153 Numărul 152 Numărul 151 Numărul 150 Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 171
Abonamente

Autonomie fără autoritate: securitatea unui harness AI în data engineering

Adrian Chitulea
Senior Data Engineer @ RebelDot



PROGRAMARE


Am făcut la un moment dat curat în allowlistul harnessului, fișierul în care scrie ce are voie să ruleze fără să mă întrebe. Se adunaseră acolo lucruri pe care le aprobasem pe rând, fiecare rezonabil în ziua în care l-am pus. Un python -c, ca să nu confirm de douăzeci de ori un script de o linie. Un gh api fără scope, ca să meargă mai repede partea cu PR-urile. Luate separat, niciunul nu m-a îngrijorat. Împreună, făceau restul controalelor decorative. Problema nu este python, ci faptul că execuția arbitrară moștenește credențialele sesiunii și ajunge direct la capabilități reale, fără să treacă prin poarta prin care trec toolurile MCP. Un agent care poate rula un script ad-hoc nu mai are nevoie de aprobare pentru nimic altceva.

M-a deranjat mai puțin că erau acolo și mai mult că le pusesem eu, treaz, cu motive bune, și nu le mai văzusem de luni de zile. Am scos ce trebuia scos și am pus un check care pică validarea dacă vreuna dintre ele reapare.

Banca Transilvania

Checkul acela păzește conținutul listei, atât. Altceva păzește execuția, și aceea este cea mai utilă decizie de securitate din harness, care nici ea nu este scrisă în vreun prompt: o linie de config care spune că modul de permisiuni nu poate fi ocolit, nici de agent, când crede că știe mai bine, nici de mine la unsprezece noaptea când vreau doar să meargă. Poate folosi doar toolurile din allowlist, rulează sub o identitate a lui cu drepturi mici, iar orice acțiune care schimbă ceva, cum ar fi pornirea unui job, o scriere sau ridicarea unei resurse în cloud, se oprește și cere aprobare. Citirea datelor reale reprezintă altă categorie: nu schimbă nimic, dar nici nu intră în accesul implicit. Nu pentru că n-am încredere în model. Pentru că încrederea nu ține loc de control.

Fluxul în sine nu are nimic spectaculos. Pornește de la un story din Jira, își scrie planul, modifică fișiere, rulează testele, se conectează prin MCP la Databricks ca să verifice o schemă sau să citească outputul unui run, iar la final deschide un PR, își face un review și actualizează documentația. În practică este mai puțin ordonat de atât. O bună parte din bătaia de cap constă în faptul că un notebook pornit din alt notebook nu poate scrie task values, deci trebuie să îi trimiți un caller_mode ca să știe în ce mod rulează și să nu încerce să scrie task values, iar libs/ trebuie sincronizat separat în workspace, altfel copilul importă altceva decât crezi tu că a importat. Nu contează aici. Contează că merge în buclă, fără să aprob eu fiecare pas, și că întrebarea nu mai este dacă a scris cod bun, ci se referă la ce poate atinge pe drumul de la story la PR și ce rămâne în urmă dacă o ia greșit pe la mijloc.

Același flux, cu al doilea strat peste el. Înăuntrul agentului nu există nicio poartă. Fiecare săgeată care iese din el trece prin una, iar rezultatul unui tool se întoarce în context ca input în care nu ai încredere.

Diagrama de mai sus arată același drum, dar cu controalele peste el. Nu pașii mă interesează acolo, ci locurile unde intră text din afară, unde agentul cere ceva de la un sistem real și unde scoate ceva în lume.

Contextul nu are tipuri

Mi-a luat ceva să înțeleg că fereastra de context nu are tipuri de date. Story-ul, fișierele din repo, un config file, outputul unui job, un comentariu de pe un PR, un mesaj de eroare de la Databricks, toate ajung în același loc și arată la fel. Text. Nimic nu vine cu o etichetă care să spună că asta este dată și nu instrucțiune, iar modelul nu are de unde să o pună singur.

Un story este scris de altcineva, uneori copiat dintr-un email, uneori editat de trei oameni în două săptămâni. Dacă agentul îl citește ca pe o comandă de încredere, atunci oricine poate scrie într-un ticket poate scrie, indirect, și în planul de execuție. Și nici nu trebuie să vină de la un om: o documentație veche, un log care conține o propoziție ce sună a cerință, rezultatul unui query.

Analogia dintre prompt injection și SQL injection este uzată, știu, și totuși lumea se întoarce la ea, probabil pentru că reprezintă singurul loc unde am avut vreodată o soluție curată. Ai prepared statements, separi codul de valori, mergi mai departe. Pentru un prompt nu există așa ceva. Nu poți lega un story ca valoare și să fii sigur că nu va fi citit ca instrucțiune, poți doar să reduci ce se întâmplă dacă se citește așa. Este incomod și riscant să nu pot închide clasa de bug, ci doar să o împrejmuiesc și să sper că am pus gardul pe unde trebuie.

Și nu trebuie să apară un atacator ca să doară. Cel mai des se strică din cooperare: un câmp de Technical Notes scris ambiguu este suficient ca agentul să facă, cu cele mai bune intenții, altceva decât aveam nevoie. Un atac real doar exploatează aceeași proprietate, mai deliberat.

Datele de care agentul nu are de fapt nevoie

Referitor la partea de acces, mă interesează mai puțin dacă agentul știe să nu atingă datele reale și mai mult dacă are nevoie de ele. Pentru cele mai multe taskuri de development nu are nevoie de rânduri reale. Are nevoie de schemă, de tipuri, de cheile de MERGE, de câteva masked samples și de niște statistici. Cât timp scrie transformarea, atât îi trebuie. Se schimbă la verificare: ca să înțelegi de ce a ieșit prost un run, te uiți la rânduri, la ce a scris jobul, uneori la payloadul care a intrat, și acolo nu mai vorbim de metadate. Este citire, deci nu trece prin poarta pentru mutații, dar merită tratată separat și nu strecurată în accesul implicit. Diferența dintre a-i da metadate și a-i da un profil confortabil peste producție nu se vede în output, se vede când ceva merge prost.

Practic înseamnă identitate separată pentru agent, drepturi de citire pe un catalog de dev și nu pe unul real, secretele în secret manager și niciodată în repo.

Există și direcția inversă, pe care o văd discutată mult mai rar. Un harness care scrie PR-uri, review-uri și documentație are canale către exterior, numai că nimeni nu le tratează ca pe canale de date. Un mesaj de eroare copiat într-un PR body poate căra un fragment de payload. Un exemplu pus în documentație ca să fie clar poate conține un nume real dintr-un set de screening. Un log verbose ajunge într-un artefact de CI la care are acces mai multă lume decât la tabelul din care a venit informația. Și mai este unul pe care nu îl numeri ca ieșire: tot ce intră în context pleacă la inferență, deci pentru date sensibile contează nu doar ce publici mai departe, ci și ce trimiți acolo.

Al doilea diff

Regulile care contează trebuie să se aplice mecanic, nu prin memoria modelului. O instrucțiune de tipul "nu face asta" reprezintă doar o preferință: ține până când un input concurent spune altceva sau până când contextul se citește altfel decât te așteptai. În schimb, ai permisiuni, medii separate, allowlist explicit pe toolurile MCP și hooks care opresc o comandă înainte să ruleze. Ai și o validare pe ceea ce iese, înainte să ajungă într-un PR sau într-un doc. Diferența este dată de faptul că acestea funcționează și când modelul greșește.

Ceea ce s-a schimbat în felul meu de a face review este că am acum două diffuri de citit. Unul este codul. Celălalt se referă la ceea ce poate face agentul: ce tooluri are, cu ce identitate rulează, ce declanșează fără confirmare, ce vede din context. Al doilea are și el istoric în git, cel puțin partea versionată din el. Allowlistul local nu este în repo, așa că acolo checkul de validare este singurul care ține minte. De asemenea, în acest istoric se observă cel mai bine cum se erodează un design. Permisiunea periculoasă nu este aceea gândită la început, ci este cea adăugată la șapte seara ca să nu se blocheze un run. Poți avea cod perfect corect și o arhitectură nesigură pentru simplul motiv că ai lărgit accesul o dată și nu ai mai revenit.

De aceea, îmi tot repet că autonomia nu trebuie să însemne autoritate. Cu cât agentul duce mai mult singur, cu atât are mai mult sens să fie îngrădit explicit. Nu urmăresc un agent care nu greșește - nu cred că se poate - , ci un sistem în care greșeala lui iese ca task eșuat și nu ca incident. Deocamdată nu este așa pe tot fluxul. Al doilea diff este acela pe care încă îl citesc prea târziu.

LANSAREA NUMĂRULUI 171

Human-in-the-loop: Keeping people at the center of AI

Miercuri, 23 septembrie, ora 18:00

Betfair Romania Development

Facebook Meetup StreamEvent YouTube

NUMĂRUL 166 - AI for Programmers

Sponsori

  • Banca Transilvania
  • Betfair
  • MHP
  • .msg systems
  • P3 group
  • Cognizant Softvision
  • BMW TechWorks Romania

INTERVIURI