ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
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 169
Abonamente

MCP cu restricții gramaticale: securizarea execuției LLM-urilor prin intermediul DSL-urilor protejate de Oauth

Adrian Roșian
Founder and owner @ WhiteCity Code



PROGRAMARE

Dacă ați petrecut ceva timp construind agenți IA pregătiți pentru producție, probabil că v-ați confruntat cu o mare problemă de securitate legată de accesul la instrumente agentice. În arhitectura software clasică, avem limite clare și statice care separă codul executabil de datele brute introduse de utilizator. Dar atunci când construiți aplicații în jurul unui LLM, aceste limite devin inutile. LLM-urile procesează prompturile sistemului, contextul dinamic preluat și intrările nesigure ale utilizatorului în exact aceeași fereastră de context. Acest design face ca sistemele să fie extrem de vulnerabile la injectarea de prompturi: un atacator poate crea cu ușurință text de intrare care suprascrie instrucțiunile sistemului și deturnează motorul de raționament al modelului.

Această criză de încredere devine periculoasă atunci când doriți, spre exemplu, să dați acces unui LLM direct la componente de sistem într-o arhitectură de cloud prin intermediul unui MCP. Dacă serverul MCP expune o suită standard de instrumente de diagnoză, brute (cum ar fi ping_node, fetch_routing_table, sau restart_service) pentru gestiunea acțiunilor de asigurare a stabilității unei aplicații în cloud, vă confruntați cu o dublă problemă:

Aceasta este problema clasică de confuzie de autor: agentul acționează cu autoritatea deplină a serverului backend, dar execută o intenție malițioasă transmisă de un utilizator neautorizat.

Soluția este de a schimba complet granița de securitate. În loc să permitem LLM-ului să execute cod arbitrar sau să orchestreze zeci de taskuri granulare de diagnostic, expunem un singur task extrem de constrâns, care acceptă un program scris într-o gramatică formală a unui DSL de micro-orchestrare. LLM-ul trimite un mini-script de diagnosticare cu instrucțiuni de ramificare explicite și predefinite. Serverul MCP la distanță analizează acest script, verifică conformitatea sa de securitate în raport cu domeniile de aplicare OAuth 2.1 validate ale utilizatorului și execută întregul bloc de diagnosticare în siguranță într-o singură tranzacție locală, de ordinul milisecundelor.

Planul arhitectural pentru micro-orchestrare sigură

În loc să permitem LLM-ului să coordoneze acțiuni complexe ale sistemului prin apeluri API separate, implementăm un strat de execuție constrâns gramatical. Ciclul de viață decuplează raționamentul în limbaj natural al LLM de interacțiunea directă cu mediul:

Securizarea granițelor de ID și transport cu OAuth 2.1

Un interpretor DSL securizat este inutil dacă clienții neautorizați pot accesa mediul de execuție. Deoarece serverele MCP la distanță operează prin transporturi bazate pe HTTP, acestea trebuie să respecte standardele formale de autorizare definite în specificația MCP, care se aliniază strict cu OAuth 2.1.

Limitarea gramaticii conform cu domeniile de autorizare și evaluarea AST

Domeniile de autorizare OAuth nu sunt doar pentru blocarea punctelor finale - în arhitectura noastră, acestea modelează dinamic gramatica expusă modelului și guvernează modul în care este evaluat AST. Am împărțit această abordare de apărare în profunzime în două straturi cheie:

Limitarea gramaticală în momentul inițierii apelului

Când clientul interoghează lista de instrumente de la serverul MCP, serverul inspectează domeniile de autorizare ale tokenurilor OAuth active ale apelantului. În loc să ofere exact aceeași gramatică tuturor, serverul elimină dinamic din schema EBNF din metadate instrucțiuni gramaticale bazat pe domeniile de aplicare specifice ale utilizatorului.

De exemplu, dacă un utilizator are doar mcp:diag:read în domeniul de aplicare, gramatica returnată va defini doar cuvinte cheie care nu sunt generatoare de mutații, cum ar fi ping, get_status și if_running. Motorul de generare ghidată al LLM este limitat fizic și nu poate genera cuvinte cheie care generează modificări, cum ar fi restart, deoarece acestea nu există în gramatica activă.

Gating la runtime

Dacă un atacator ocolește constrângerile de pe partea clientului, interpretorul de pe partea serverului acționează ca o limită rigidă de tip zero-trust. Fiecare nod din AST-ul analizat este verificat în raport cu domeniile de aplicare validate ale utilizatorului înainte de orice execuție:

Unelte de parsing: FParsec pentru interpretoare sigure

F# este un limbaj incredibil de puternic pentru scrierea de compilatoare și interpretoare sigure, datorită sistemului său strict de tipuri și imutabilității. Pentru a analiza DSL-ul nostru securizat de micro orchestrare, putem folosi FParsec, o librărie de parser combinators matură și extrem de optimizată, construită special pentru F#.

FParsec are o abordare de sus în jos, recursive-descent. Aceasta înseamnă că reușim să construim parsere complexe prin compunerea unor funcții modulare mici, folosind operatori de combinare clari. Librăria acceptă nativ gramaticile LL(k) și LL() cu look-ahead arbitrar, și revenirea în stream* folosind operatorul attempt atunci când o ramură eșuează.

Deoarece FParsec folosește recursive -descent, trebuie să țineți cont de două aspecte importante de proiectare:

Plan tehnic: Implementarea unui parser-interpreter în F

Iată cum arată o implementare completă, type-safe, scrisă în F# funcțional pur, folosind librăria FParsec. Începem prin a defini tipurile de date ale domeniului, claims pentru user și AST-ul nostru de micro orchestrare (care acceptă liste de comenzi, recuperarea din erori în try-catch și ramificarea condiționată) folosind uniunile discriminate și tipurile de date din F#:

namespace SecureMcp

open FParsec

type Scope =
    | DiagRead
    | DiagWrite

type UserClaims = {
    UserId: string
    Scopes: Scope list
}

type Command =
    | Ping of host: string
    | GetStatus of service: string
    | RestartService of service: string

type DiagAST =
    | Action of Command
    | Sequence of DiagAST list
    | TryCatch of tryBlock: DiagAST * catchBlock: DiagAST
    | IfRunning of service: string * thenBlock: DiagAST * elseBlock: DiagAST

type ExecutionError =
    | UnauthorizedTable of string
    | FieldAccessDenied of field: string * value: string
    | InsufficientScope of requiredScope: string
    | ParserFailure of string

Apoi, scriem parser-combinatorul folosind sintaxa optimizată de pipeline de operatori a FParsec. Folosim createParserForwardedToRef pentru a gestiona în mod clar definițiile recursive necesare pentru ramificarea condiționată și blocurile try-catch:

    let strLiteral : Parser =
      pchar '"' >>. manyCharsTill anyChar (pchar '"')
    let ws = spaces
    // Base diagnostic action parsers
    let pingParser : Parser =
      pstring "PING" >>. ws >>. strLiteral |>> 
      (fun host -> Action (Ping host))

    let statusParser : Parser =
      pstring "GET_STATUS" >>. ws >>. strLiteral 
      |>> (fun svc -> Action (GetStatus svc))

    let restartParser : Parser =
      pstring "RESTART" >>. ws >>. strLiteral |>> 
      (fun svc -> Action (RestartService svc))

    let actionParser : Parser =
      choice [ attempt pingParser; attempt 
      statusParser; restartParser ]

    // Setup forward reference for recursive   
    // branching rules
    let expr, exprImpl = 

createParserForwardedToRef()
    // Parses: IF_RUNNING "service" THEN {... } ELSE {... }
    let ifRunningParser : Parser =
     pstring "IF_RUNNING" >>. ws >>. strLiteral.>> ws
      .>> pstring "THEN".>> ws.>>. expr.>> ws
      .>> pstring "ELSE".>> ws.>>. expr
      |>> fun ((svc, thenB), elseB) -> 
      IfRunning(svc, thenB, elseB)

    // Parses: TRY {... } CATCH {... }
    let tryCatchParser : Parser =
      pstring "TRY" >>. ws >>. expr.>> ws.>> 
      pstring "CATCH".>> ws.>>. expr
      |>> fun (tryB, catchB) -> TryCatch(tryB, 
      catchB)

    // Parses block wrappers: { cmd1 cmd2 }
    let blockParser : Parser =
      between (pchar '{'.>> ws) (ws.>> pchar '}') 
      (sepBy expr ws)
      |>> Sequence

    // Resolve the recursive parser implementation
    do exprImpl := choice
    // Complete program entry point
    let diagParser : Parser = ws >>. 
      expr.>> ws.>> eof

Într-un final, implementăm interpretorul server-side. Acesta evaluează recursiv AST-ul de diagnostic, verificând domeniile de autorizare ale identității validate ale apelantului la fiecare nod și returnează un jurnal secvențial al operațiunilor:

module Interpreter =
  type Env = {
    RunningServices: string list
    PingResult: string -> bool
   }
  let rec eval (ast: DiagAST) (claims: UserClaims) (env: Env) : Result =
    match ast with
      | Action cmd ->
        match cmd with
        | Ping host ->
   if not (List.contains DiagRead claims.Scopes) then
     Error (InsufficientScope "mcp:diag:read")
       else
         let ok = env.PingResult host
         Ok
       | GetStatus svc ->
   if not (List.contains DiagRead claims.Scopes) then
      Error (InsufficientScope "mcp:diag:read")
        else
          let running = List.contains svc env.
          RunningServices
            Ok
          | RestartService svc ->
  if not (List.contains DiagWrite claims.Scopes) then
       Error (InsufficientScope "mcp:diag:write")
         else
           Ok
       | Sequence asts ->
         let rec evalSeq acc remaining =
         match remaining with
           | -> Ok (List.concat (List.rev acc))
           | head :: tail ->
            match eval head claims env with
              | Error err -> Error err
              | Ok logs -> evalSeq (logs :: acc) tail
            evalSeq asts
        | TryCatch (tryB, catchB) ->
            match eval tryB claims env with
            | Ok logs -> Ok logs
            | Error _ ->

      // Attempt automatic containment/recovery 
      //locally on the server
       match eval catchB claims env with
       | Ok catchLogs -> Ok ( "CONTAINMENT_TRIGGERED" 
       :: catchLogs )
       | Error err -> Error err
       | IfRunning (svc, thenB, elseB) ->
   if not (List.contains DiagRead claims.Scopes) then
         Error (InsufficientScope "mcp:diag:read")
       else
         let running = List.contains svc env.RunningServices
    if running then

     eval thenB claims env
    else
      eval elseB claims env

Ciclul de viață JSON-RPC pentru execuția DSL

Comunicarea dintre aplicația gazdă, clientul MCP și serverul la distanță respectă protocolul structurat de mesaje JSON-RPC 2.0. Iată o hartă completă a fluxului care prezintă modul în care se execută descoperirea capabilităților și apelurile de instrumente securizate:

Mesajele JSON-RPC structurate fac ca aceste limite de comunicare să fie clare și standardizate:

1. Descoperirea capacităților

Clientul solicită o listă de instrumente disponibile de la server:

{
  "jsonrpc": "2.0",
  "id": "req_001",
  "method": "tools/list",
  "params": {}
}

Serverul răspunde, publicând toolul execute_diagnostic_pipeline expunând schema gramaticală trunchiată, indicând că acceptă doar șiruri de caractere care corespund sintaxei noastre de execuție:

{
  "jsonrpc": "2.0",
  "id": "req_001",
  "result": {
    "resultType": "complete",
    "tools":,
          "additionalProperties": false
        }
      }
    ]
  }
}

2. Invocarea toolului (happy-path)

Modelul gazdă scrie un program recursiv conceput pentru a verifica un serviciu, a da un ping unui endpoint API și a declanșa un bloc de repornire dacă oricare dintre etape eșuează:

{
  "jsonrpc": "2.0",
  "id": "req_002",
  "method": "tools/call",
  "params": {
    "name": "execute_diagnostic_pipeline",
    "arguments": {
      "program": "TRY { IF_RUNNING \"database\" THEN { PING \"api.internal\" } ELSE { RESTART \"database\" } } CATCH { RESTART \"database\" }"
    }
  }
}

Serverul analizează, interpretează și evaluează pipeline-ul într-o singură tranzacție cu datele disponibile. Deoarece tokenul utilizatorului conține ambele domenii de autorizare diag:read și diag:write, programul se execută și returnează un rezultat pozitiv:

{
  "jsonrpc": "2.0",
  "id": "req_002",
  "result": {
    "resultType": "complete",
    "content":"
      }
    ]
  }
}

3. Invocarea toolului (path de eșec datorat permisiunilor insuficiente)

Dacă LLMul emite un program care încearcă să execute o mutație restart, dar tokenul utilizatorului nu are în domeniul de autorizare mcp:diag:write, interpretorul de pe server oprește execuția la acel nod specific:

{
  "jsonrpc": "2.0",
  "id": "req_003",
  "result": {
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "Execution rejected: operation requires scope 'mcp:diag:write'"
      }
    ]
  }
}

Viitorul sistemelor agentice sigure

Consolidarea securității agenților bazați pe LLMuri necesită mutarea graniței de încredere de la prompting nedeterminist în limbaj natural la abordări bazate pe teoria clasică a compilatoarelor.

Prin constrângerea interfeței LLM-ului printr-un DSL scris într-o gramatică formală se diminuează matematic întreaga suprafață de atac disponibilă. Chiar dacă un atacator reușește o injecție de prompturi excelentă, progresul acestuia este complet limitat de către sintaxa DSL-ului. Sintaxa F# strictă și semantica clară de interpretare bazată pe parser-combinators din FParsec contribuie la soliditatea absolută a oricărei implementări de gramatici cu sau fără trunchiere de instrucțiuni ghidate de domenii de autorizare OAuth. Acest lucru vă oferă un framework compatibil cu conceptele de cod auditabil, zero-trust, și pregătit pentru IA în producție.

NUMĂRUL 166 - AI for Programmers

Sponsori

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

INTERVIURI