Da por hecho que un identificador se ha filtrado
// Progress only. This tool is reachable by any caller holding a site id, so
// it must not hand back the owner's email or the raw webhook payload.
$response = [
'id' => $site->id,
'url' => $site->url,
'status' => $site->status,
'is_live' => $site->status === 'completed',
'stages' => ProvisioningStages::checklist(/* … */),
'updated_at' => $site->updated_at?->toIso8601String(),
];
Una herramienta de estado recibe un identificador y devuelve un registro. El instinto es devolver el registro. Pero un identificador viaja —está en una conversación, puede estar en un registro, puede acabar pegado en un chat de soporte— y una herramienta expuesta a un modelo está expuesta a cualquier cosa que pueda hablar con ese modelo.
Así que devuelve progreso y nada más. Ni el correo del propietario, ni la carga externa en crudo, ni los metadatos que el registro lleve por casualidad.
Fíjate también en lo que admite el comentario: una ruta HTTP hermana estaba acotada así y a este camino simplemente nunca se le dio el mismo trato. Así es como aparecen estas brechas de verdad —no por una mala decisión, sino por un segundo punto de entrada a los mismos datos que nadie pensó en alinear. Cuando añadas una herramienta sobre datos existentes, ve a leer qué decidió ocultar el endpoint existente.
Algunas herramientas deben existir y no ofrecerse nunca
protected array $tools = [
// …
// Note: ResetLeadTool intentionally excluded from public tools.
// It's internal-only for debugging/development.
];
Una herramienta que reinicia el estado de un contacto es genuinamente útil en desarrollo y es, en un conjunto publicado, una forma de que una conversación persuasiva deshaga una verificación.
Registrar es una decisión. Que el fichero exista no es consentimiento, y «es sólo para depurar» no es un control —el comentario documenta un control que impone el array que hay encima.
Rechaza de forma útil
Todos los rechazos aquí siguen diciéndole al llamante qué hacer:
return Response::json([
'verified' => false,
'error' => 'Maximum verification attempts exceeded',
'reason' => 'Maximum 3 attempts allowed per email address',
'attempts_used' => 3,
'attempts_remaining' => 0,
'action' => 'Request a new verification code using SendVerificationEmailTool',
'retry_after_minutes' => 15,
]);
attempts_remaining permite al modelo avisar al usuario antes del último intento. action nombra la herramienta que debe llamar en su lugar. retry_after_minutes le da algo concreto que decir.
Sin esto, un modelo bloqueado improvisa: se disculpa, reintenta la misma herramienta o se inventa un tiempo de espera. Las respuestas de seguridad siguen siendo parte de la conversación, y un rechazo que es sólo una negativa produce peor comportamiento que uno que redirige.
Lo que sostiene este capítulo
- La complacencia es la superficie de ataque. Lo que impongas pidiéndoselo amablemente al modelo no está impuesto.
- Cuenta intentos contra el recurso, no contra el intento. Indexar por el valor enviado da a cada intento un presupuesto nuevo.
- Usa una ventana corta y una larga. Un tope por minuto sólo modera el abuso; el tope diario es el que acota el daño.
- Incrementa atómicamente. Leer-modificar-escribir permite superar un tope poco a poco.
- Pregunta qué confirma cada error a quien está adivinando. Nunca devuelvas la identidad a la que pertenece una credencial, y haz que todo rechazo cueste un intento.
- Una vinculación hecha al emitir gana a un argumento aportado al usar. Donde una alternativa sea inevitable, revalídala contra algo ya demostrado.
- Da por hecho que cualquier identificador se ha filtrado. Una herramienta de estado devuelve progreso, no el registro.
- Al añadir una herramienta sobre datos existentes, lee qué ocultó el endpoint existente. Estas brechas vienen de segundos puntos de entrada, no de malas decisiones.
- Registrar es una decisión. Algunas herramientas deben existir y no publicarse nunca.
- Rechaza de forma útil —intentos restantes, la herramienta que llamar en su lugar y cuánto esperar.