Action is proposed as structured data (tool + args).
Проблема (з реального продакшену)
Агент — “просто текст”. Поки ти не дав йому tools.
Як тільки агент може викликати tools, ти побудував permission систему. Хочеш ти цього чи ні.
І неприємна правда: prompts — не permissions. “будь ласка, не роби X” — не control layer.
Чому це ламається в продакшені
1) “Дамо доступ, обмежимо потім” закінчується інцидентом
Якщо ти думаєш “guardrails додамо після того, як доведемо value” — це вступ до postmortem.
2) Tools — це capabilities, а не “функції”
db.write — це не “функція”.
Це capability: вона змінює state.
Side effects бувають дорогі або незворотні.
Проєктуй permissions як capability graph:
- read vs write
- scoped ресурси (tenant/project/user)
- limits (rate/cost/steps)
3) Default-allow = шлях до витоків
Default-allow + один неправильний tool call і ти маєш:
- PII у логах
- writes у неправильне середовище
- cross-tenant доступ
Приклад реалізації (реальний код)
Мінімальна модель:
- capability-based allowlist
- scope checks (tenant_id)
- write tools опційно за approvals
from dataclasses import dataclass
@dataclass(frozen=True)
class ToolPermission:
name: str
scopes: set[str] # e.g. {"tenant:acme"}
mode: str = "allow" # allow | approve
class PolicyDenied(RuntimeError):
pass
def check_permission(perms: list[ToolPermission], *, tool: str, scope: str) -> ToolPermission:
for p in perms:
if p.name == tool and scope in p.scopes:
return p
raise PolicyDenied(f"policy_denied:{tool}:{scope}")export class PolicyDenied extends Error {}
export function checkPermission(perms, { tool, scope }) {
for (const p of perms) {
if (p.name === tool && p.scopes.includes(scope)) return p;
}
throw new PolicyDenied("policy_denied:" + tool + ":" + scope);
}Реальний інцидент (з цифрами)
Агент мав доступ до “admin” DB wrapper’а, бо так було “зручніше”. Він мав робити triage support тікетів.
Prompt injection у тікеті змусив його виконати запит, який повернув більше даних, ніж треба.
Імпакт:
- зовнішнього витоку не було (пощастило)
- але PII потрапили в внутрішні логи (це все одно інцидент)
- ~6 годин інциденту + cleanup + audit
Fix:
- least privilege на кожен tool (scoped creds)
- redaction outputs у tool gateway
- allowlist + approvals для writes
Компроміси
- Permission модель коштує інженерного часу. Це дешевше, ніж час інцидентів.
- Більше scope’ів = більше менеджменту policy.
- Занадто жорстко може гальмувати продукт → потрібні хороші defaults і зрозуміла ескалація.
Коли НЕ варто
- Навіть для read-only tools потрібні scopes (tenant boundaries).
- Якщо tool не можна scope’нути (він може “все”) — виправ tool, а не policy.
Чекліст (можна копіювати)
- [ ] Capability-based назви tools (read vs write)
- [ ] Default-deny allowlist
- [ ] Scoped credentials per tenant/user/project
- [ ] Approval mode для irreversible writes
- [ ] Output redaction (PII) + logging
- [ ] Stop reasons: policy_denied
Безпечний дефолтний конфіг (JSON/YAML)
tool_permissions:
default: "deny"
grants:
- tool: "kb.read"
scopes: ["tenant:acme"]
mode: "allow"
- tool: "ticket.close"
scopes: ["tenant:acme"]
mode: "approve"
logging:
redact_outputs: true
FAQ (3–5)
Використовується в патернах
Пов’язані відмови
Q: Чому prompt не достатній як permission?
A: Бо prompt не enforce. Модель може помилятись, галюцинувати або бути injected.
Q: Scopes реально потрібні?
A: Так, якщо ти multi-tenant або маєш кілька середовищ. Інакше витік станеться рано чи пізно.
Q: Як комбінувати permissions і budgets?
A: Permissions відповідають “можна?”, budgets — “скільки часу/скільки $?”. Потрібні обидва.
Q: Де enforce це все?
A: У tool gateway. Не в UI і не в prompt.
Пов’язані сторінки (3–6 лінків)
- Foundations: How agents use tools · Stateless vs stateful agents
- Failure: Prompt injection attacks · Tool response corruption
- Governance: Allowlist vs blocklist · Human approvals
- Production stack: Production agent stack