BookinglyTech News
Software

TenantInvariant: una crate de Rust para que el modelo no decida de quién es el recurso

La librería convierte el aislamiento entre clientes en una invariante ejecutable antes de que el agente ejecute la herramienta: el modelo propone el ID, el servidor resuelve el dueño.

3 min de lecturaDev.to0 vistas

Un agente de soporte atiende a un usuario de tenant-a. El usuario pregunta por un contrato y el modelo emite una llamada perfectamente válida: get_contract("contract-b"). La herramienta existe y el argumento tiene la forma correcta. El problema es que ese contrato pertenece a tenant-b. Un desarrollador que firma como subaru ha publicado tenant-invariant, una crate experimental de Rust que saca esa comprobación del prompt y la coloca como invariante ejecutable justo antes de que se ejecute la llamada.

El fallo no se arregla con un prompt mejor. El autor lo dice sin rodeos: un prompt más afinado puede reducir la frecuencia del error, pero no lo convierte en una frontera de autorización. La tentación es meter tenant_id y contract_id en el esquema de la herramienta y dejar que el modelo rellene ambos. Eso le da voz en algo que no le corresponde: el identificador de tenant puede venir del prompt de sistema, de un resultado anterior o de texto escrito por el usuario, y ninguna de esas fuentes prueba autoridad.

El modelo propone, el servidor resuelve

La regla que adopta la librería es corta: el modelo puede proponer un ID de recurso, pero no puede decirle a la aplicación quién es el dueño de ese recurso. El tenant del actor sale del contexto autenticado en servidor; el propietario del recurso lo resuelve la aplicación contra su base de datos o contra otro servicio de confianza. Solo se comparan esos dos valores. Si la búsqueda falla, el resultado es deny: no hay información suficiente para autorizar, así que falla cerrado.

En el código, check_tenant recibe un Actor y un ResourceOwner y devuelve Decision::Allow o Decision::Deny(DenyReason::CrossTenant). TenantId rechaza un identificador vacío y ResourceOwner::Unknown hace visible un fallo de resolución en lugar de esconderlo en un nulo. El autor reconoce que la comparación podría ser un if y no pretende disimularlo: lo útil de empaquetarlo como crate es que la regla tiene nombre y tipos, y que el flujo de control se vuelve difícil de ignorar. Se instala con cargo add tenant-invariant y el repositorio trae un ejemplo ejecutable que imprime blocked: CrossTenant.

Los tests que importan

Además de los casos obvios —mismo tenant permite, otro tenant deniega, dueño desconocido deniega—, hay escenarios para un tenant forjado en los argumentos generados por el modelo y para un lote con recursos de varios clientes. Ese último es fácil de romper: validar el primer elemento no debe autorizar el resto del lote. La prueba más interesante es de propiedades, con proptest generando pares de identificadores en lugar de elegirlos a mano. La propiedad es casi el producto: si los IDs difieren, la decisión tiene que ser CrossTenant siempre.

Queda por ver si esto duplica lo que ya existe. El autor lo plantea abiertamente: los guardrails del SDK de agentes de OpenAI y los controles de uso de herramientas de Anthropic cubren parte del terreno, y la duda de si la crate aporta algo encima de ellos no queda resuelta en el texto. Es experimental, tiene un mantenedor y no basta con añadirla para estar a salvo: representa estados inválidos de forma explícita, nada más. Para quien construye agentes con acceso a datos multiinquilino, la idea de que el prompt no es una frontera de seguridad es el recordatorio que vale la pena llevarse.