Identity приходит из сессии, не из аргументов модели
Модель может выбрать order_id - это её работа. Но она не должна сообщать actor_id, tenant или уровень доступа: эти значения формируются сервером из проверенной сессии и замыкаются внутри handler, куда модель дотянуться не может. Разница принципиальная: order_id - это намерение, а identity - это право, и право не приходит из вероятностного текста.
В коде это значит, что handler получает доверенный контекст отдельным аргументом, а запрос к базе всегда сужен по actor и tenant.
type TrustedToolContext = {
actorId: string;
tenantId: string;
locale: "ru" | "en" | "uz";
traceId: string;
};
const orderReadInput = z.object({
order_id: z.string().min(6).max(40)
}).strict();
async function orderRead(
raw: unknown,
context: TrustedToolContext
) {
const input = orderReadInput.parse(raw);
const order = await orders.findOne({
id: input.order_id,
actorId: context.actorId,
tenantId: context.tenantId
});
if (!order) return { found: false };
return {
found: true,
order_id: order.id,
status: order.publicStatus,
refundable: order.refundable
};
}Именно эта конструкция обезвреживает самую частую атаку на агентов.
Prompt injection не получает новых прав. Текст пользователя или RAG-фрагмент может просить игнорировать правила, изменить tenant или вызвать скрытую функцию. Но если handler не принимает эти поля из аргументов и проверяет ownership в запросе, такая инструкция остаётся просто текстом - она не становится разрешением.
Чтобы почувствовать, какой класс риска у tool и что он требует, прогоните его через классификатор.
Классификатор риска tool
Класс: read. Выполнение после server-side authorization.
Tools описаны и защищены по правам. Теперь свяжем их в цикл - и главное требование к нему в том, чтобы он был коротким и наблюдаемым.
Откуда берётся actor_id при вызове tool?