Back to blog
AI Agents

AI Tool Permission Model: Least Privilege in Practice

Bind each execution to one task

Limit actor, target and effect in the worker

Original PERMIT-7 contract and support ticket example
Permission failures and production tests
Primary nodePermission decision gate
Routing modePERMIT-7
StatusPUBLISHED
An AI tool request passes through scoped permissions, review and a verified destination
PERMIT_7_V01: bind actor, target, effect and verified result.
TERMINAL_PREVIEW.LOG
$ permit --contract PERMIT-7
> bind: actor / tenant / task
> gate: tool / target / effect
> route: allow / review / deny
Architecture, failure modes and production checks

An AI model may propose a tool call, but it must not decide its own authority. A permission model for AI agents binds each call to an authenticated actor, tenant, task, target and allowed effect. This guide focuses on that decision boundary. For broader delivery needs, see the AI specialist in Armenia page.

The original PERMIT-7 design below uses a synthetic support workflow: an agent reads one permitted ticket and proposes a draft reply. It is an architecture example, not a claim about a deployed client system. Related guides cover tool schemas, MCP server security and agent sandboxing.

Problem and requirements

A tool catalog often exposes actions such as read_ticket, update_ticket and send_reply. Giving the agent a broad service token turns a prompt error into a possible cross-tenant read or external write. A prompt instruction cannot enforce authorization at the destination. The server must validate every request with current identity and resource policy, including retries.

Start with a narrow effect. Reading a ticket, drafting text, updating a status and sending a reply are four distinct permissions. Define the target object, fields, purpose, expiry and review requirements for each. Default to denial when identity, policy version or target ownership cannot be established.

PERMIT-7 architecture

GateContractEvidence
1. Actorauthenticated user and tenantsession or delegated identity
2. Taskapproved purpose and expirytask ID and scope
3. Toolexact name and versioncatalog entry
4. Targetobject ID resolved by serverownership and ACL result
5. Effectread, draft, update or sendexplicit capability
6. Reviewhuman approval for material writesbound approval receipt
7. Resultdestination read-backaudit receipt or unknown outcome

The orchestrator supplies identity to a policy decision point. The model provides only candidate arguments. A trusted adapter resolves the ticket ID, checks the actor's access and applies an allowlisted operation. The adapter never accepts a token, tenant ID or unrestricted URL supplied by the model. The destination system remains the ultimate authority.

Synthetic policy and call

yaml
actor: support-agent-user-42
tenant: example-tenant
purpose: draft_reply
resource: ticket:784
allow:
  - ticket.read
  - reply.draft
expires_at: 2026-09-26T18:00:00Z
requires_human_for:
  - reply.send
  - ticket.update

This manifest illustrates a contract; it is not a production policy file. A real system derives identity and permissions from trusted services and evaluates fresh ACLs at call time. The model can request reply.send, but this manifest alone cannot authorize it.

Components and execution path

The catalog publishes narrow tool schemas. An argument validator rejects unknown fields, malformed IDs and unbounded lists. The policy engine checks actor, tenant, purpose, target and effect together. A review service binds any approval to an exact action and payload hash. The adapter uses a short-lived delegated credential or its own scoped identity and records the destination result. Logs retain IDs and decisions, not raw sensitive ticket text.

ts
// Illustrative pseudocode; server obtains actor and tenant outside the model.
const proposal = toolSchema.parse(modelCall.arguments);
const target = await ticketStore.resolve(proposal.ticketId);
const decision = await policy.evaluate({ actor, tenant, taskId, target, effect: "reply.draft" });
if (!decision.allowed) return deny(decision.reason);
const draft = await adapter.createDraft(target, proposal.body, requestId);
return verifyDraftAtDestination(draft.id, requestId);

For reply.send, a separate approval step must bind the intended recipient and exact content. After approval, check permissions again: access may have been revoked while a draft waited. An idempotency key and destination receipt protect retries, but an unknown result still needs reconciliation before another send.

Failure modes

FailureConsequenceRequired route
Model supplies another tenant's ticket IDcross-tenant readserver-side ACL denial
Tool name aliases a broader operationunintended writeversioned catalog and exact effect mapping
Approval references an older draftchanged message sentbind approval to payload hash
Permission revoked after planningstale authorizationrecheck immediately before effect
Timeout after sendingduplicate reply on retryreconcile with destination receipt
Logs include raw ticket bodiesdata exposureredact and limit retention

Do not confuse process isolation with data authorization. The sandbox guide limits code, files and network; this policy controls which business object and effect an authorized tool may reach.

Tests and production checklist

Test permitted reads, denied cross-tenant reads, unknown fields, stale policy, expired task, revoked role, changed draft after approval and retries after uncertain writes. Assert both the API response and the destination state. Include a test where a malicious retrieved document instructs the model to call a broader tool; the tool gate must still deny it.

Before production, inventory tools and effects, assign policy owners, define review thresholds, rotate scoped credentials, specify audit retention and rehearse revocation and reconciliation. Monitor denied calls, approval mismatches and unknown outcomes as operational signals, not invented success rates. Prompt engineering can narrow the model's proposed actions; AI automation covers workflow ownership. For a scoped architecture review, contact the team.

CODE_BLOCK.TXT
const target = await store.resolve(proposal.ticketId);
const decision = await policy.evaluate({ actor, tenant, taskId, target, effect });
if (!decision.allowed) return deny(decision.reason);
const result = await adapter.executeOnce(target, requestId);
return verifyAtDestination(result.id);