Cómo construir un compilador de contexto seguro para agentes de IA en .NET 10
Compila el contexto del modelo en .NET 10 con etiquetas de confianza, controles de vigencia, redacción de datos, cuarentena de inyecciones, presupuestos de tokens deterministas y un manifiesto SHA-256 comprobable.
Cuando un agente se porta mal en producción, la gente suele culpar al modelo. Muchas veces el daño se hizo antes, cuando la aplicación pegó instrucciones, resultados de búsqueda, memoria y texto web no fiable en una única cadena privilegiada. Esa cadena es todo lo que el modelo sabe para la siguiente inferencia. Si una política obsoleta se impone a la vigente, una página descargada trae instrucciones, datos de clientes cruzan una frontera o un historial de poco valor se come la ventana de contexto, un prompt más ingenioso arriba no va a reparar la arquitectura.
La solución es tratar ese paso de ensamblado como software de verdad. Cada entrada recibe una identidad y un nivel de confianza. Las políticas deciden qué es lo bastante reciente y qué hay que ocultar. Un presupuesto fijo obliga a tomar decisiones explícitas. La salida es reproducible y va acompañada de pruebas de qué se incluyó, recortó, redactó, puso en cuarentena o descartó. A continuación, esa idea se convierte en un pequeño servicio de .NET 10.
Por qué no basta con concatenar
Los textos de ingeniería de Anthropic presentan el context engineering como la curación de todo el estado del contexto en cada inferencia, algo que va mucho más allá de pulir la redacción de un solo prompt. Las instrucciones del sistema, las herramientas, los datos recuperados, la memoria y el historial de mensajes compiten por un único recurso finito, y las ventanas más grandes no eliminan los problemas de relevancia y contaminación.
La seguridad agudiza el problema de selección. El artículo de OpenAI de marzo de 2026 sobre cómo resistir la prompt injection señala que los ataques capaces se parecen cada vez más a la ingeniería social, así que un clasificador de entradas por sí solo no es una defensa completa. También propone pensar en términos de fuentes y sumideros: por dónde entra la influencia no fiable y a qué capacidad peligrosa puede llegar. El trabajo PlugMem de Microsoft Research añade un ángulo de fiabilidad y muestra que la recuperación de memoria en bruto puede inundar a un agente con contexto largo y de poco valor.
La conclusión práctica es que los datos necesitan un contrato de ingesta, controles de política, un presupuesto y una procedencia antes de llegar a un modelo.
La forma del compilador
TrustedContextCompiler expone un único endpoint:
POST /v1/context/compile
La petición lleva la tarea, las instrucciones de sistema de confianza y una lista de fuentes identificadas, cada una con su tipo, nivel de confianza, prioridad y hora de recuperación. Lo que vuelve es el contexto ensamblado, una estimación aproximada de tokens, una huella SHA-256 que cubre exactamente ese texto y un manifiesto con una decisión por fuente. La canalización de procesamiento es así:
flowchart LR
A[Retrieved sources] --> V[Validate ID and freshness]
V --> T[Apply trust policy]
T --> Q[Quarantine suspicious untrusted data]
T --> R[Redact sensitive values]
R --> B[Deterministic budget allocator]
B --> C[Compiled context]
B --> M[Decision manifest]
C --> F[SHA-256 fingerprint]
En la estructura del proyecto, la política está separada del transporte:
TrustedContextCompiler/
├── src/TrustedContextCompiler.Core/
├── src/TrustedContextCompiler.Api/
├── tests/TrustedContextCompiler.Tests/
├── samples/compile-request.json
├── docs/verification.md
├── Dockerfile
└── TrustedContextCompiler.slnx
La biblioteca principal no depende de ningún modelo. Es deliberado, porque la política de contexto debería ser determinista y comprobable sin gastar tokens ni esperar a un proveedor.
Identidad y confianza para cada fuente
No se aceptan cadenas anónimas. Cada entrada está tipada:
public enum TrustLevel
{
Trusted = 0,
Internal = 1,
User = 2,
Untrusted = 3
}
public sealed record ContextSource(
string Id,
string Kind,
string Content,
TrustLevel Trust,
int Priority,
DateTimeOffset RetrievedAt);
Id vincula la salida con las pruebas de recuperación. Kind separa políticas, tickets, memoria, páginas web y otros tipos de fuente. Priority es una entrada acotada para la política, y RetrievedAt alimenta la decisión de vigencia.
La etiqueta de confianza tiene que venir del código de ingesta y nunca del propio documento, para que una página web no pueda proclamarse de confianza. Una fila de una base de datos interna tampoco es fiable automáticamente. La etiqueta debería describir quién puede influir en el contenido y qué ruta de validación lo produjo.
Se rechazan los ID duplicados, al igual que los campos que faltan y las prioridades fuera del rango de 0 a 100. Fallar pronto importa aquí, porque un manifiesto construido sobre identidades ambiguas da una falsa sensación de seguridad.
Las instrucciones reciben su presupuesto primero
Antes de tener en cuenta ningún dato recuperado, las instrucciones de confianza y la tarea reciben una parte reservada del presupuesto:
var instructionBlock =
quot;<trusted-instructions>\n" +
quot;{request.SystemInstructions.Trim()}\n" +
quot;Task: {request.Task.Trim()}\n" +
quot;</trusted-instructions>";
var instructionTokens = EstimateTokens(instructionBlock);if (instructionTokens > options.ReservedInstructionTokens)
{
throw new ArgumentException(
"Instructions exceed the reserved instruction budget.",
nameof(request));
}
Así una avalancha de resultados de búsqueda no desplaza en silencio el plano de control. También impide que un prompt de sistema sobredimensionado se coma todo el presupuesto de datos sin un error de configuración visible.
Los tokens se cuentan con una estimación simple de cuatro caracteres por token:
public static int EstimateTokens(string value) =>
Math.Max(1, (value.Length + 3) / 4);
No es un tokenizador apto para facturación. En producción usarías el tokenizador del modelo elegido y dejarías un margen de seguridad para los envoltorios de mensajes y la codificación específica de cada proveedor. Una aproximación determinista sigue sirviendo para probar el comportamiento de las políticas, siempre que se etiquete con honestidad.
Descartar el contexto obsoleto
Una fuente puede ser válida y aun así inadecuada para la tarea porque es antigua. Cada una se compara con una antigüedad máxima configurable:
if (now - source.RetrievedAt >
TimeSpan.FromMinutes(options.MaxSourceAgeMinutes))
{
manifest.Add(Record(
source,
ContextDecision.Stale,
0,
0,
hash,
"Source exceeded the freshness policy."));
continue;
}
En un sistema más grande, la vigencia debería variar según el tipo de fuente. Una política de reembolsos puede necesitar una antigüedad máxima corta, mientras que un manual de producto puede seguir estable durante meses. Cuando el sistema de referencia proporcione una versión o un intervalo de validez, prefiérelo. La hora de recuperación es un recurso de reserva y no demuestra que el contenido esté al día.
La decisión de diseño importante es que las fuentes obsoletas permanecen en el manifiesto. Desaparecen del contexto del modelo, pero siguen en el registro operativo.
Poner en cuarentena los intentos de inyección en datos no fiables
Solo las fuentes Untrusted pasan por una heurística de inyección. Busca patrones concretos: intentos de anular instrucciones de mayor prioridad, de extraer secretos o de colar marcado con estilo de roles.
if (source.Trust == TrustLevel.Untrusted &&
SafetyFilters.Injection().IsMatch(source.Content))
{
manifest.Add(Record(
source,
ContextDecision.Quarantined,
0,
0,
hash,
"Untrusted content matched an " +
"instruction-injection heuristic."));
continue;
}
Una coincidencia pone en cuarentena la fuente entera. Recortar una sola frase sería frágil, así que el compilador sacrifica algo de exhaustividad a cambio de una decisión fácil de explicar.
La expresión regular no es ni de lejos un "cortafuegos de IA".
Redacción antes de que el modelo vea nada
Las fuentes que superan la cuarentena se transforman antes del reparto del presupuesto. El ejemplo detecta direcciones de correo, tokens Bearer y valores con forma de tarjeta de pago:
public static (string Value, int Count) Redact(string value)
{
var count = 0;
value = Email().Replace(value, _ =>
{
count++;
return "[EMAIL]";
}); value = Bearer().Replace(value, _ =>
{
count++;
return "[SECRET]";
}); value = Card().Replace(value, _ =>
{
count++;
return "[CARD]";
}); return (value, count);
}
Como la redacción ocurre antes de la inclusión, el valor original nunca llega al contexto compilado. El manifiesto solo guarda el número de redacciones y un hash SHA-256 de la fuente original.
Estos patrones son ejemplos y no un sistema DLP completo. Los identificadores internacionales, los números de cuenta, los secretos propios de cada empresa y los datos personales en formato libre necesitan detectores adecuados. Además, un hash del contenido puede delatar un valor de baja entropía si alguien lo adivina, así que, donde esa amenaza importe, usa un HMAC con una clave protegida.
Un presupuesto determinista
Los candidatos se ordenan por prioridad de mayor a menor, después por mayor confianza y después por ID de fuente:
foreach (var candidate in candidates
.OrderByDescending(item => item.Source.Priority)
.ThenBy(item => item.Source.Trust)
.ThenBy(item => item.Source.Id, StringComparer.Ordinal))
{
// Allocate the remaining budget.
}
El desempate por ID no es decorativo. Sin él, dos resultados de búsqueda con la misma puntuación podrían producir prompts, claves de caché y comportamientos del modelo distintos según el orden en que los devolvieran la base de datos o la red.
Cada fuente tiene una asignación máxima. La que no cabe se recorta a lo que queda del presupuesto, y aquella cuyo envoltorio por sí solo no cabe se descarta. Ambos resultados aparecen en el manifiesto. Cada fuente se envuelve con su ID, su tipo y sus metadatos de confianza:
<source id="ticket-42" kind="ticket" trust="internal">
Contact [EMAIL]. Refund requested: $125.
</source>
Los marcadores ayudan a la separación y la observabilidad, pero dentro del modelo no son una frontera criptográfica. Un modelo aún puede malinterpretar los datos o seguirlos, así que los controles de herramientas deben asumir que algunos ataques al contexto funcionarán.
Poner huella al artefacto
Tras el ensamblado determinista, el compilador calcula el hash del texto UTF-8 exacto del contexto:
var context = string.Join("\n\n", sections);
var fingerprint = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(context)));
La huella permite claves de caché seguras, comparaciones de regresión y correlación con las llamadas al modelo, todo sin registrar el contexto completo. Una clave de caché en producción también debería incluir la versión de la política del compilador y el identificador del modelo, ya que el mismo texto con una política o un tokenizador distintos puede no significar lo mismo.
El manifiesto se ordena por ID de fuente para que las comparaciones sean estables. Para cada fuente registra la confianza, la decisión, los tokens incluidos estimados, el número de redacciones, el hash del contenido original y el motivo.
La capa de ASP.NET Core
Una Minimal API añade lo que un servicio compilador necesita en producción:
- Opciones fuertemente tipadas, validadas al arrancar
- Enums como cadenas en el contrato JSON expuesto a quien llama
- Un limitador de peticiones de ventana fija
- Health checks, con los errores devueltos como Problem Details
- Logs estructurados generados en el código fuente, que contienen recuentos y nunca contexto
- Cancelación, además de los límites de petición estándar de ASP.NET Core
La autenticación se deja fuera a propósito. Antes de que el endpoint sea accesible para nadie, añade identidad de carga de trabajo o de servicio y autorización por inquilinos, tipos de fuente y perfiles de política.
Pruebas que comprueban decisiones
Cinco pruebas cubren las afirmaciones principales:
- Una fuente no fiable que parece una instrucción queda en cuarentena y no aparece en el contexto.
- Los correos y los valores de tarjeta se sustituyen antes de incluirse.
- Pasar las entradas en orden inverso da el mismo contexto y la misma huella.
- Cuando se agota el presupuesto, una fuente de menor prioridad se recorta o se descarta.
- Una fuente obsoleta está en el manifiesto, pero no en el contexto.
La prueba de determinismo es breve y muy importante:
var left = compiler.Compile(Request(first, second));
var right = compiler.Compile(Request(second, first));
Assert.Equal(left.Fingerprint, right.Fingerprint);
Assert.Equal(left.Context, right.Context);
La solución se ejecutó en .NET 10 sobre Windows. El restore funcionó, la compilación Release no dio advertencias ni errores y las cinco pruebas pasaron. Después, una comprobación en vivo arrancó la API y envió una petición con tres fuentes: una política de confianza, un ticket interno con una dirección de correo y una nota web no fiable con un ataque mediante instrucciones.
Errores que conviene evitar
- Dejar que una fuente elija su propio nivel de confianza
- Tratar las etiquetas delimitadoras como un sandbox
- Registrar todo el contexto por comodidad
- Usar una puntuación de búsqueda como autoridad ilimitada
- Descartar fuentes en silencio, sin manifiesto
- Llamar defensa completa contra inyecciones a un detector heurístico
- Presentar un recuento aproximado de tokens como cifra de facturación
Una regla más: no ajustes el contexto aislado de las herramientas. Una frase no fiable es tan peligrosa como la capacidad que puede dirigir, y descargar un documento público es un sumidero muy distinto de enviar datos de clientes, así que cada uno necesita su propia autorización.
Qué ganas con esto
El contexto es más que texto inofensivo: lo que entra en él se convierte en el mundo en el que piensa un agente, y una idea no fiable puede dirigir lo que haga a continuación. Un compilador así no hará desaparecer la prompt injection. Lo que hace es eliminar los accidentes. Las fuentes tienen identidad, la confianza se fija fuera del contenido, los datos antiguos se rechazan, los valores sensibles se transforman, los presupuestos son predecibles, las entradas sospechosas quedan en cuarentena y cada decisión se puede inspeccionar. El context engineering deja de ser artesanía de prompts y se convierte en una disciplina de backend que se puede probar. Además, quienes revisan obtienen un artefacto concreto que examinar cuando el comportamiento del agente cambia entre versiones.