Keep AI tool selection separate from authorization
Let a local model choose an action, then check identity, resource access and exact arguments in application code before executing the tool.

A selected tool still needs permission
I want our agent to add an internal note to a ticket. Adding notes is an allowed action, but the current user cannot access this ticket. The model can select the right tool for that request and still propose an operation we must deny.
Ardana returns a selected option from the names we supply. That response grants no authorization and carries no executable arguments. I keep the permission check in the application component that executes the tool. OWASP’s agent guidance places that check outside the agent context, before the operation has an effect.
A model supplies intent. Our application authorizes the exact operation.
For this ticket, I resolve the current user from trusted session information and restrict the lookup to tickets they may access, following OWASP’s IDOR guidance. I then check whether that user may add a note to this ticket. OWASP’s authorization guidance calls for denying by default and checking permissions on every request. Our action names can stay small and explicit while the application enforces those permissions.
Give the model a bounded set of action names
Our application owns the action names. A choice question accepts between 2 and 255 distinct options, supplied as names or as names with descriptions. Those names give the caller a fixed vocabulary to dispatch against. The descriptions tell the model what each option means; they do not implement the operation.
I would send this example request for our ticket. It matches the API contract, but I have not executed it. It asks for a private note and gives the model a read action, a note action, and an option to call no tool.
{
"model": "decider:2b",
"state": {
"request": "Add a private note to ticket T-17"
},
"questions": {
"action": {
"type": "choice",
"instructions": "Choose the next action.",
"criteria": {
"read_ticket": "Read the ticket",
"add_internal_note": "Add a private note",
"defer": "Do not call a tool"
}
}
}
} The question identifier is ours too. Ardana accepts a map of questions and returns each answer under its original identifier, so I can look up the answer for this action question by the same key. The choice answer includes the selected name, confidence, and probabilities. I use that name to identify the requested operation in application code.
I need to handle the no-tool option explicitly. Ardana copies the supplied name of the highest-probability option into the choice answer; it does not automatically abstain. I would wire the no-tool option to an application handler that stops dispatch.
Our other options need application handlers as well. The inference handler returns a decision, with no executable arguments or authorization grant in the choice response. I treat its probabilities as information about the selection, never as permission to add the note. Before the application invokes the selected handler, it still needs to decide whether the exact operation is allowed.
Authorize the exact operation before it runs
For this design, I put an executor between the selected action and the ticket handler. Ardana’s choice response carries neither executable arguments nor an authorization grant. The calling application owns the handler and must check scope, privilege and required approval before invoking it. OWASP places those checks in the execution component, outside the agent context.
Actor identity comes from the application’s trusted session. I resolve the ticket within the resources that actor can access, following OWASP’s object access guidance. A submitted ticket identifier tells the application which object was requested; it establishes no permission. I also check whether this actor may perform this operation on that ticket, with access denied by default on every request, as OWASP recommends.
Dispatch uses a fixed map of allowed action names to application handlers. I require an exact match and reject unknown names before any handler runs. For the arguments, syntactic validation checks types, formats, required fields and unexpected fields. Semantic validation checks whether the values make sense together for the requested operation under the application’s business rules. These checks establish acceptable input; authorization still needs its own decision, as OWASP’s input validation guidance explains.
Our inference service has a separate access check. Ardana’s optional shared bearer key controls access to inference. It does not establish ticket ownership or permission to add a private note, so I keep those checks in the application even when the inference request passed authentication.
The same separation applies when I dispatch through MCP. Its tools specification requires servers to validate inputs and enforce access controls. Tool annotations describe intended behavior without guaranteeing it; the maintainers put enforcement in authorization, transport or runtime controls. The executor still has to authorize the particular operation it is about to run.
Recheck when the target or approval changes
I treat a tool call waiting for execution as unfinished work. The actor can lose access while it waits, and the ticket or arguments can change after review. That leaves a gap between what we checked and what we are about to run. OWASP’s transaction authorization guidance describes this time-of-check/time-of-use risk and calls for a final check tied to execution. For our executor, that means checking the operation we will perform using the permissions that apply when it runs.
We also need to know exactly what an approval covers. Bind it to the actor, tool, target resource, normalized parameters, timestamp, and expiry, as OWASP’s approval guidance specifies. A different ticket or changed arguments require renewed approval. Approval for the earlier call cannot carry over just because the tool name stayed the same.
The execution component must independently check scope, privilege, and required approval outside the agent context. It must verify and consume that approval atomically immediately before execution, so concurrent calls cannot each reuse it. OWASP’s execution controls also call for replay protection and idempotency for consequential operations.
I want tests at this boundary before relying on it. OWASP’s testing guidance includes unauthorized tool use, privilege escalation, and approval bypass, even when the model requests the action confidently. We should record the authorization outcome separately from the execution result and include the policy version in the logs. Those records let us ask whether a request was denied or an authorized operation failed.
Try the request against your executor
I want to try the ticket request through the local server first. Install Ardana and pull the model before starting the server. Keep your application executor available too, because that is where we need to test the ticket operation.
curl -fsSL https://ardana.ai/install.sh | sh
ardana pull decider:2b Save the JSON request from the earlier section as request.json. This example asks for a private note on a ticket and has not been executed. With that file ready, we can start the local inference server.
ardana serve Leave the server running and send the saved request from another terminal. I keep this step separate from the application call so I can inspect the action choice first. The response supplies no executable arguments or permission to change a ticket.
curl http://127.0.0.1:8000/v1/systemone -H 'content-type: application/json' -d @request.json # CLI alternative
ardana run decider:2b --request request.json --json Now try the note operation against a permitted ticket and a denied ticket in your executor tests. I want to see the permitted note written and the denied ticket untouched. Include a confident unauthorized request, privilege escalation, and an attempt to bypass required approval. These cases follow OWASP’s authorization testing guidance.
Keep authorization outcomes separate from execution results in the logs, and record the policy version. That gives us distinct evidence for the permission check and the attempted operation. Open the Ardana repository, run the request, and wire the selected action into those executor tests.