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. 1.Identify scope
  2. 2.Inspect evidence
  3. 3.Apply host control
  4. 4.Run negative cases
  5. 5.Compare benign behavior
  6. 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

host.mjs
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.