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.