Sections in this lesson
Agent Tool Authorization & Approval
Treat model tool arguments as untrusted input.
Reviewed: · Content owner: Interview Preparator
What you will learn
- A proposal is not a grant
- Bind approval to the effect
- Handle uncertainty after timeout
Before you start
The flow
- 1.Identify scope
- 2.Inspect evidence
- 3.Apply host control
- 4.Run negative cases
- 5.Compare benign behavior
- 6.Review remaining risks
A proposal is not a grant
Treat model tool arguments as untrusted input. The host binds authenticated tenant, user and run identity, validates the exact action/resource shape and rechecks resource access before executing. A model cannot choose a different privileged identity by naming it.
Read and write require different authority. The fixture permits authorized reads but requires a host-stored write grant matching payload digest, scope, run, epoch and expiry. This is a teaching grant record, not a signed token verifier.
Bind approval to the effect
An approval for PLAN-1 must not authorize PLAN-2 or a changed action. Expired, revoked or incident-invalidated grants fail closed. Authenticated human approval must come from a trusted service, not text in a model message or editable JSON.
Idempotency binds a call identifier to its payload at the target boundary. In-memory receipts disappear on restart; the earlier JDBC example demonstrates mutation and receipt in one database transaction.
Handle uncertainty after timeout
A timed-out write may still have happened. Do not retry blindly or assume cancellation reversed an external effect. Query a durable receipt or reconcile the target state under a defined policy.
The fixture has synchronous counters and no external write endpoint. Production leases, distributed delivery, signed/short-lived grants and target-service idempotency remain explicit integration work.
Try it
import { authorize, payloadDigest } from './policy.mjs';// Teaching host: no shell, network, or arbitrary-file tools are exposed.export class TeachingHost { constructor({ scope, resource, grant, maxCalls = 3, maxTokens = 100, deadline = 1000, epoch = 1 }) { if (!Number.isSafeInteger(maxCalls) || maxCalls < 1 || !Number.isSafeInteger(maxTokens) || maxTokens < 1 || !Number.isFinite(deadline)) throw new Error('Invalid budget'); this.scope = Object.freeze({ ...scope }); this.resource = structuredClone(resource); this.grant = grant ? structuredClone(grant) : null; this.maxCalls = maxCalls; this.maxTokens = maxTokens; this.deadline = deadline; this.epoch = epoch; this.calls = 0; this.tokens = 0; this.mutations = 0; this.receipts = new Map(); this.trace = []; } execute(call, callId, reservedTokens, now) { if (!Number.isSafeInteger(reservedTokens) || reservedTokens < 1 || !Number.isFinite(now) || !/^[a-z0-9-]{1,80}$/.test(callId)) throw new Error('Invalid operation'); if (now >= this.deadline) return this.record('TIMEOUT'); const permission = authorize(this.scope,call,this.resource,this.grant,now,this.epoch); if (permission !== 'ALLOW') return this.record(permission); const digest = payloadDigest(call); const cached = this.receipts.get(callId); if (cached) return this.record(cached.digest === digest ? 'REPLAY' : 'PAYLOAD_MISMATCH'); if (this.calls >= this.maxCalls || reservedTokens > this.maxTokens - this.tokens) return this.record('BUDGET_EXCEEDED'); this.calls++; this.tokens += reservedTokens; if (call.action === 'completePlan') this.mutations++; this.receipts.set(callId,{ digest }); return this.record('EXECUTED'); } record(status) { this.trace.push({ status, calls:this.calls, tokens:this.tokens, mutations:this.mutations }); return status; } stopAndRevoke() { this.epoch++; this.grant = null; }}// Cache keys include authorization scope and corpus version; cache values still// require current authorization at read time (revocation/deletion can change).export function cacheKey(scope, corpusVersion, question) { return JSON.stringify([scope.tenant,scope.user,corpusVersion,question]); }export function memoryRead(record, scope, version) { return record.tenant === scope.tenant && record.user === scope.user && record.conversation === scope.conversation && record.trusted === true && record.version === version ? record.text : null;}Run npm run examples:security and npm run eval:security. This source is a deterministic teaching fixture; it does not prove live model safety, NLI, production auth or OS/network isolation.
Interview questions
Can a model choose the caller identity?
Read and write require different authority. The fixture permits authorized reads but requires a host-stored write grant matching payload digest, scope, run, epoch and expiry. This is a teaching grant record, not a signed token verifier.
What invalidates a write approval?
Idempotency binds a call identifier to its payload at the target boundary. In-memory receipts disappear on restart; the earlier JDBC example demonstrates mutation and receipt in one database transaction.
Does a timeout prove no write occurred?
The fixture has synchronous counters and no external write endpoint. Production leases, distributed delivery, signed/short-lived grants and target-service idempotency remain explicit integration work.
Practice
Extend one negative scenario and its authorized positive counterexample for Agent Tool Authorization & Approval.
Check your solution
- State the trusted input and effect boundary.
- Assert the expected status and observed effect.
- Report coverage and limitations without claiming production readiness.
Official references
Continue learning
Category sequence
AI Security
Lesson 6 / 14
Was this lesson useful?
Change notes: 2026-10-31: security/reliability curriculum and source-bound fixture.