TSM - 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

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.