Publicado el 5 min de lectura

Revisa los cambios JSON de la IA con una lista de campos permitidos

En esta página

Un asistente de IA al que pides desactivar las notificaciones por correo también podría “mejorar” la configuración de reintentos. Ambos documentos pueden ser JSON válido y cumplir el mismo esquema. Importa tanto qué cambió como que el resultado pueda analizarse.

Construye un control de solo lectura que compare una configuración original con su reemplazo propuesto y rechace cambios fuera de un único campo autorizado. Este tutorial usa Python 3.10 o posterior, módulos de la biblioteca estándar y casos de prueba locales sintéticos. No llama a ningún modelo ni modifica archivos.

Dos documentos de configuración detrás de un marco de inspección de vidrio, con un cambio de campo resaltado en turquesa y una propuesta ámbar fuera del marco.

1. Define el cambio permitido

Guarda el documento original y la lista de permisos en un estado controlado por la aplicación. Pide al asistente un documento candidato completo que conserve todos los campos excepto notifications.email. No permitas que el candidato redefina sus propios permisos.

Usa el JSON Pointer exacto /notifications/email, no un prefijo como /notifications. RFC 6901 define las rutas JSON Pointer y sus escapes: una barra dentro de una clave se convierte en ~1, y una tilde en ~0. Por tanto, una clave literal llamada a/b es distinta de las claves anidadas a y b.

Este control presupone documentos JSON pequeños, ya validados, con claves de texto y números finitos. Antes de conectar respuestas del modelo sin procesar, aplica controles estrictos de entrada JSON. El decodificador predeterminado de Python acepta claves duplicadas; su documentación de JSON explica los mecanismos de personalización pertinentes.

2. Guarda el control de comparación

Guarda lo siguiente como audit_json_edits.py. Los objetos se comparan recursivamente. Añadir o eliminar miembros cuenta como cambio, incluso si el valor eliminado era nulo. Los arrays son indivisibles: cambiar cualquier elemento o su orden marca el campo del array completo.

La comparación mediante serialización ordenada ignora el orden de las claves de los objetos y distingue booleanos de números, también dentro de arrays. Además, considera distintos 1 y 1.0. Es una regla conservadora de este flujo, no una definición universal de igualdad JSON ni una canonicalización criptográfica.

import json
from copy import deepcopy

BASELINE = {
    "notifications": {"email": True},
    "retry_limit": 3,
    "channels": ["email", "sms"],
}
ALLOWED = {"/notifications/email"}


def serialized(value):
    return json.dumps(value, sort_keys=True, allow_nan=False)


def changed_paths(before, after, path=""):
    if isinstance(before, dict) and isinstance(after, dict):
        changes = set()
        for key in sorted(before.keys() | after.keys()):
            token = key.replace("~", "~0").replace("/", "~1")
            child = path + "/" + token
            if key not in before or key not in after:
                changes.add(child)
            else:
                changes.update(changed_paths(before[key], after[key], child))
        return changes
    return {path} if serialized(before) != serialized(after) else set()


def decision(candidate):
    changes = changed_paths(BASELINE, candidate)
    blocked = changes - ALLOWED
    if blocked:
        return "REJECT " + ", ".join(p or "(root)" for p in sorted(blocked))
    if type(candidate["notifications"].get("email")) is not bool:
        return "REJECT email must be boolean"
    return "ALLOW " + (", ".join(sorted(changes)) or "(unchanged)")


approved = deepcopy(BASELINE)
approved["notifications"]["email"] = False
overreach = deepcopy(approved)
overreach["retry_limit"] = 9
missing = deepcopy(BASELINE)
del missing["notifications"]["email"]
wrong_type = deepcopy(BASELINE)
wrong_type["notifications"]["email"] = 0

for name, candidate in [
    ("unchanged", BASELINE),
    ("approved", approved),
    ("overreach", overreach),
    ("missing", missing),
    ("wrong_type", wrong_type),
]:
    print(f"{name}: {decision(candidate)}")

assert changed_paths({"a/~b": 1}, {"a/~b": 2}) == {"/a~1~0b"}
assert changed_paths({"x": [True]}, {"x": [1]}) == {"/x"}
assert changed_paths({"x": None}, {}) == {"/x"}
assert changed_paths(BASELINE, []) == {""}
assert changed_paths(BASELINE, deepcopy(BASELINE)) == set()
print("edge checks: PASS")

La comprobación de permisos y la de valores tienen objetivos distintos. Cambiar solo una ruta autorizada tampoco permite sustituir su booleano por cero ni eliminar el campo obligatorio.

3. Ejecuta y valida los casos de prueba

Desde el directorio que contiene el script, ejecuta:

python3 audit_json_edits.py

Salida estándar esperada:

unchanged: ALLOW (unchanged)
approved: ALLOW /notifications/email
overreach: REJECT /retry_limit
missing: REJECT email must be boolean
wrong_type: REJECT email must be boolean
edge checks: PASS

Este ejemplo se comprobó localmente con Python 3.11.4: código de salida 0, sin salida de error estándar y exactamente el resultado mostrado. Estos resultados prueban el control, no la fiabilidad de ningún modelo.

El caso de cambio excesivo contiene la modificación de correo solicitada y otra no relacionada en el límite de reintentos. Se rechaza la propuesta completa; no apliques automáticamente su parte “buena”. Las aserciones cubren claves escapadas, un cambio de booleano a número dentro de un array, la eliminación de un nulo, el reemplazo del documento completo y una copia sin cambios. Sigue el mismo enfoque de casos negativos al probar SQL generado por IA.

4. Resuelve problemas antes de conectar la escritura

  • Se rechaza una ruta padre: sustituir el objeto completo de notificaciones por un escalar cambia /notifications. El permiso sobre una hoja no autoriza ese reemplazo.
  • Se rechaza un cambio de array: permite el array completo solo si ese permiso más amplio es intencional; este ejemplo no autoriza índices individuales.
  • Se permite no cambiar nada: el control comprueba el alcance, no la finalización de la tarea. Exige por separado que el valor final del correo sea False cuando la tarea requiera desactivarlo.
  • La entrada es demasiado grande o profunda: recházala antes de comparar. Este ejemplo didáctico recursivo no es una barrera de seguridad para entradas sin límites.

En una integración real, valida el esquema completo del candidato, comprueba la autorización y detén el proceso ante errores de análisis o comparación. Mantén inmutable el candidato comprobado y rechaza originales desactualizados mediante una comprobación de versión dentro de la transacción de escritura. La aprobación debe corresponder al mismo documento que finalmente guardes. Una vista previa de renombrado de archivos sin escritura aplica la misma separación entre propuesta y ejecución.

Conclusión práctica

Un esquema indica qué puede contener un documento; una lista de campos permitidos indica qué puede cambiar esta operación concreta. Comprueba ambos, revisa la diferencia propuesta y mantén la escritura separada del modelo.

Artículos relacionados

Últimos artículos