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.