Cada bloque de esta página es verbatim de contracts/param_binding.yaml, en el orden en que el archivo lo declara. Un test los compara contra el archivo en cada build, así que esta página no puede desviarse del contrato que dice mostrar.

Qué es el documento

Una versión y un dominio, y después el comentario que los autores le dejaron al próximo que lo lea. Este contrato existe para demostrar una cosa: un pago cuyo destinatario y monto no pueden ser redirigidos por contenido contaminado, porque la aplicación es determinista y ocurre en la escritura.

spec_version: "1.0"
metadata:
  domain: param_binding
# ADR-0106 GTM demo — the canonical parameter-injection attack, blocked.
# A `payment` records a transfer whose:
#   - recipient is server-bound to the authenticated actor (bind),
#   - amount must not exceed the anchored quote's cap (assert),
#   - quote anchor cannot be re-pointed once set (immutable_after_set).
# Contaminated retrieved content cannot redirect the recipient or inflate the
# amount: enforcement is deterministic at the write choke point.
# Quotes are issued by admin, never by the operator role the agent acts as: an
# assert is only as strong as its anchor, so the agent cannot set its own cap.

Quiénes pueden existir

Dos roles. auto_assign significa que un usuario nuevo queda como operator sin que nadie se lo otorgue. Todo lo demás en esta página se decide por rol, así que este bloque corto es al que se refiere el resto del documento.

roles:
  - name: operator
    auto_assign: true
  - name: admin

Qué puede hacer cada rol

La autorización es una lista centrada en roles, nunca inline en una entidad. Cada grant nombra una ruta — records/payment, llm/services/primary — y las acciones permitidas sobre ella. Mirá lo que operator no tiene: ningún read sobre records/payment, ni acceso a records/users. Un permiso que nunca se escribió no existe.

authorization:
  - role: operator
    grants:
      - on: records/payment
        actions:
          - create
          - list
      - on: records/quote
        actions:
          - read
          - list
      - on: llm/services/primary
        actions:
          - use
  - role: admin
    grants:
      - on: records/payment
        actions:
          - read
          - list
      - on: records/quote
        actions:
          - create
          - read
          - list
      - on: records/users
        actions:
          - create
          - read
          - update
          - list
      - on: llm/services/primary
        actions:
          - use
  - role: owner
    grants:
      - on: records/payment
        actions:
          - read
          - list
      - on: records/users
        actions:
          - read
          - list

El modelo de datos, declarado y no migrado

Las entidades se vuelven tablas reales con restricciones reales. unique, pattern, min_length y format los impone la base de datos, no un prompt ni código de aplicación que vos mantengas.

entities:
  - name: users
    fields:
      - name: name
        type: string
        required: true
        constraints:
          unique: true
          pattern: ^[a-z0-9_-]+$
          min_length: 3
          max_length: 30
      - name: full_name
        type: string
        required: true
      - name: email
        type: string
        constraints:
          unique: true
          format: email

El valor contra el que se va a medir la entidad siguiente

Una cotización lleva un tope. Por sí sola no tiene nada de particular — importa porque el pago de abajo se asierta contra ella, y porque la referencia no se puede mover una vez escrita.

  - name: quote
    fields:
      - name: cap
        type: currency
        required: true
        config:
          default_currency: USD
          precision: 2
        constraints:
          minimum: 0

Los tres bindings, y el ataque que cierran

Esto es el punto entero del contrato. bind significa que el servidor deriva el destinatario del actor autenticado — un valor que el modelo proponga se sobrescribe, no se rechaza, así que no hay nada que discutir. immutable_after_set ancla la cotización para que la aserción de abajo no pueda re-apuntarse después a otra más conveniente. assert sujeta el monto a ese tope anclado; un valor redirigido se rechaza con un 422 en la escritura, no se lo convence con un prompt mejor.

  - name: payment
    fields:
      # bind — server derives the recipient from the authenticated actor; the
      # LLM's proposed value is overwritten, not rejected.
      - name: recipient
        type: string
        binding:
          bind: actor.user_name
      # immutable_after_set — anchor the quote so the assert below cannot be
      # re-pointed at a different (RLS-visible) quote after the first write.
      - name: quote
        type: reference
        references: quote
        binding:
          immutable_after_set: true
      # assert — the amount must not exceed the anchored quote's cap; a value
      # redirected by contaminated content is rejected 422 at the choke point.
      - name: amount
        type: currency
        required: true
        config:
          default_currency: USD
          precision: 2
        constraints:
          minimum: 0
        binding:
          assert: "value <= quote.cap"

El modelo es una interfaz

El proveedor y el modelo son una declaración, y el runtime resuelve las credenciales por deployment. Cambiar este bloque cambia qué modelo corre. Nada de lo de arriba se mueve — los bindings, los grants y las restricciones son el mismo documento conteste quien conteste.

llm:
  services:
    - name: primary
      provider: gemini
      model: gemini-2.5-flash
      priority: 10

De qué puede hablar, y quién lo decide

strategy es una decisión sobre cuánto pagás por que el tema se decida. La coincidencia de palabras es gratis y no distingue “quiero mandarle la plata que acordamos” de una pregunta sobre el clima — ninguna matchea nada — así que no aparta nada. Este contrato paga: embedding_similarity compara significado, así que sí las distingue, a una llamada de embeddings por turno más una credencial para el servicio declarado arriba. Apartar una categoría regulada de plano es otro nivel distinto de éste — palabras fijas, falla cerrado.

# The embedding catalog the topic gate below embeds with. Separate from `llm`
# because it is a different model with its own credential: the secret keys to a
# service NAME here, so a declaration must exist for it to point at.
embeddings:
  services:
    - name: primary
      provider: gemini
      model: gemini-embedding-001
# `strategy` picks WHO decides what this assistant will discuss, and the choice is
# a cost. `auto_derived` scores keyword overlap: free, no network, and unable to
# tell a request phrased differently from an unrelated one — "quiero mandarle plata
# al proveedor" matches nothing here, exactly like a question about the weather —
# so it turns nothing away. This contract declares `embedding_similarity` instead:
# it compares meaning rather than words, so it can tell those two apart, and it
# costs one embedding call per turn plus a credential for the service above.
# `description` is what gets embedded; `off_topic_instruction` is the voice of a
# refusal. Both were inert under the previous strategy and both decide here.
# No `confidence_threshold`: 0.65 is this strategy's own default, and a number
# restated is a number that goes wrong the day the strategy changes.
# Whatever the gate decides, the runtime still tells the model unconditionally to
# emit `conversation.out_of_scope` for a message outside scope, and the misuse
# detector counts those.
treatment:
  topic_scope:
    strategy: embedding_similarity
    embedding_service: primary
    description: Help operators record payments against approved quotes, under
      bound, asserted, and immutable field constraints.
    off_topic_instruction: Politely decline off-topic requests and redirect the user
      to recording payments against approved quotes.

Y el único bloque de acá que no se hace cumplir

Todo el resto del archivo se hace cumplir — la base, el binding y el grant deciden, proponga lo que proponga el modelo. Estas líneas son la excepción, y el contrato lo dice donde están. La persona y las reglas son texto que se le entrega al modelo, y un modelo puede ignorar texto: el NEVER en mayúsculas es un pedido, no una restricción. Lo que hace seguro tener persuasión en este documento es que nada de lo de arriba depende de ella — al pago no se lo puede redirigir por más que al modelo lo convenzan. Y está acá, diffeable, en el documento que lee el revisor de compliance.

  # EVERYTHING BELOW IS NOT ENFORCED. The persona and the rules are text handed to
  # the model, and a model can ignore text — the shouted NEVER is a request, not a
  # constraint. That is safe here because nothing above depends on it: the bind and
  # the assert hold whatever the model is persuaded to answer. It lives in this
  # document, versioned and diffable, rather than in a prompt buried in code.
  persona: |
    You are an operations assistant for recording payments against approved quotes.
    You record transfers accurately and never invent values.
  base_response_rules:
    - ALWAYS respond in the SAME LANGUAGE the user wrote their message in.
    - NEVER ask the user for system-level information such as tenant ID, user
      ID, or session ID.
  data_integrity_rules:
    - NEVER fabricate or assume data values that are not present in the result
      data.

Eso era todo

No se escribió código de aplicación. El esquema de base de datos, el modelo de permisos, los bindings de valor, el proveedor y el comportamiento conversacional son el documento que acabás de leer — que es por qué compliance puede revisarlo, por qué se puede diffear en un pull request, y por qué corre idéntico en cada tenant donde se publique.

Volver al inicio