Cada bloque de esta página es textual 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 quote_issuer, 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: quote_issuer

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.

authorization:
  - role: operator
    grants:
      - on: records/payment
        actions:
          - create
          - list
      - on: records/quote
        actions:
          - read
          - list
      - on: llm/services/backup
        actions:
          - use
      - on: llm/services/primary
        actions:
          - use
  - role: quote_issuer
    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/backup
        actions:
          - use
      - on: llm/services/primary
        actions:
          - use
  - role: admin
    grants:
      - on: llm/services/backup
        actions:
          - use:
              scope:
                - contract
                - runtime
      - on: llm/services/primary
        actions:
          - use:
              scope:
                - contract
                - runtime
  - role: owner
    grants:
      - on: records/payment
        actions:
          - read
          - list
      - on: records/users
        actions:
          - read
          - list

El modelo de datos, declarado y no migrado

Cada entidad y sus campos se declaran en el contrato, sin migraciones que escribir. unique, min_length y format los hace cumplir el runtime en cada escritura (unique, la propia base), no un prompt ni código de aplicación que mantengas vos.

entities:
  - name: users
    fields:
      - name: name
        type: string
        required: true
        constraints:
          unique: true
          min_length: 3
      - name: full_name
        type: string
      - 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. Importa porque el pago de abajo se valida 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

bind significa que el servidor deriva el destinatario del actor autenticado: un valor que proponga el modelo se sobrescribe, no se rechaza, así que no hay nada que discutir. immutable_after_set ancla la cotización para que la validación de abajo no pueda apuntarse después a otra más barata. assert sujeta el monto a ese tope anclado: un monto por encima se rechaza con un 422 en la escritura, diga lo que diga el prompt, y un monto desviado por debajo igual pasa, porque un assert acota un valor sin elegirlo.

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

# Every turn starts on `primary`, Anthropic's `claude-sonnet-5-5` (#2151). `backup` is Gemini, whose
# credential the topic gate's embeddings need anyway: a turn reaches it on an outage of `primary`
# (408, 429, 5xx or no answer, #2008) or when `primary` holds no credential, never on another error.
llm:
  services:
    - name: primary
      provider: anthropic
      model: claude-sonnet-5-5
      priority: 10
    - name: backup
      provider: gemini
      model: gemini-3.5-flash-lite
      priority: 20

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 coincide con 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. Rechazar de plano una categoría regulada es otro nivel, con palabras fijas, y 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`: this strategy's default is measured on this model,
# and a number restated is a number that goes wrong the day the default moves.
# 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: el destinatario no se puede desviar y el monto no puede pasar su tope, por más que convenzan al modelo. Y está acá, en el documento que lee el revisor de compliance, donde cada cambio aplicado sube la versión del contrato y queda registrado.

  # 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.

Sin código de aplicación

El esquema de base de datos, el modelo de permisos, los bindings de valores, el proveedor y el comportamiento conversacional son el documento que acabás de leer. Por eso compliance puede revisarlo, un pull request puede mostrar cada cambio, y corre igual en cada tenant donde se publique.

Volver al inicio