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

$ permit --contract PERMIT-7
> bind: actor / tenant / task
> gate: tool / target / effect
> route: allow / review / denyAn 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
| Gate | Contract | Evidence |
|---|---|---|
| 1. Actor | authenticated user and tenant | session or delegated identity |
| 2. Task | approved purpose and expiry | task ID and scope |
| 3. Tool | exact name and version | catalog entry |
| 4. Target | object ID resolved by server | ownership and ACL result |
| 5. Effect | read, draft, update or send | explicit capability |
| 6. Review | human approval for material writes | bound approval receipt |
| 7. Result | destination read-back | audit 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
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.updateThis 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.
// 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
| Failure | Consequence | Required route |
|---|---|---|
| Model supplies another tenant's ticket ID | cross-tenant read | server-side ACL denial |
| Tool name aliases a broader operation | unintended write | versioned catalog and exact effect mapping |
| Approval references an older draft | changed message sent | bind approval to payload hash |
| Permission revoked after planning | stale authorization | recheck immediately before effect |
| Timeout after sending | duplicate reply on retry | reconcile with destination receipt |
| Logs include raw ticket bodies | data exposure | redact 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.
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);