Context-Aware Authorization for AI Agents

Your AI Agent Has Permission to Do That. That Doesn't Mean It Should.

Last year, our team was building an internal assistant on top of GPT-4 for one of our enterprise clients. The idea was straightforward enough: recruiters and hiring managers could ask questions, and the assistant would pull context from their ATS, internal Confluence docs, and an HR portal to give useful answers. We scoped out the RBAC rules carefully. Every user had their own identity, and the assistant made requests on behalf of that user. Then a coordinator on the recruiting team asked the assistant a vague question about "candidate pipeline status and team capacity" during a quarterly planning cycle. The agent, doing exactly what we told it to do, pulled a Confluence page about Q3 headcount planning that included salary band information and internal leveling notes for open roles. That page was technically accessible to that user. But no one in their right mind would have handed it over in that context. Worse, the ATS data it cross-referenced included interview feedback and rejection reasons for candidates still in active pipelines at other offices. Sensitive stuff. The kind of thing that, if it surfaced in the wrong hands, creates real legal exposure. The RBAC check passed. The intent check didn't exist. That's the problem I want to talk about. Why Traditional Access Control Breaks Down With Agents RBAC works beautifully for deterministic systems. A user clicks a button, the app checks permissions, the app either returns the data or it doesn't. The request is narrow, the scope is clear, and the access decision happens at a single, well-defined boundary. AI agents don't work that way. At all. An agent takes a loosely specified goal, breaks it into sub-tasks, decides on its own which tools to call, and chains together results across multiple systems before giving you an answer. Each individual tool call might be perfectly authorized, but the combination of those calls, assembled by a model making autonomous decisions, can surface information that was never meant to be exposed in that context. Think about it this way. A recruiting coordinator might have read access to the ATS, the headcount planning dashboard, and the HR directory. Each of those permissions is reasonable on its own. But if an AI agent acting on their behalf starts cross-referencing all three simultaneously because the user asked "who should I talk to about the stalled hires on the APAC engineering team?", you've potentially created something much closer to an executive talent intelligence briefing than a simple pipeline lookup. The permissions were all valid. The combination wasn't. That gap between authorization and intent is what keeps me up at night. The Attack Surface Nobody Talks About Enough There are three failure modes I've seen in practice, and they're all slightly different. Privilege accumulation during multi-step reasoning. The agent starts with a narrow task, but as it reasons through sub-steps, it pulls in context that progressively widens the scope. By step 4 of a 5-step chain, it's operating with far more information than the original request justified. Each individual fetch was authorized. The aggregate wasn't. In an ATS context, this might look like an agent that starts with "show me open reqs" and ends up surfacing internal compensation targets, headcount approval chains, and rejected candidate feedback, all in one response, because each intermediate step seemed like a reasonable next hop. Prompt injection via retrieved content. The agent reads a document containing instructions embedded in natural language, and those instructions hijack its behavior. The authorization layer never sees this happening because it's not a traditional API call; it's the model doing what it's told by content it retrieved. This is nastier in recruiting workflows than people realize. ATS platforms like Greenhouse, Lever, and Workday all support rich text fields for candidate notes, job descriptions, and scorecards. Any of those fields can carry embedded instructions if someone knows what they're doing. I've seen this demonstrated against Spring AI-based agents using maliciously crafted scorecard documents. Not theoretical. It happened in a demo environment we were running for a client last spring, and the look on their security lead's face was something. Ambient authority leakage. This is the sneaky one. The agent has a tool like sendEmail or createOfferLetter , and a user with limited permissions asks it to do something. The agent, trying to be helpful, uses a service account with broader permissions to complete the task. The user didn't have the authority to do the thing directly, but the agent did it anyway. In a recruiting context, picture a coordinator inadvertently triggering an offer letter workflow they have no business initiating, because the agent's service account had the permission even if they didn't. Catching this in a traditional access control model is really hard. What Context-Aware Authorization Actually Means The mental model I've settled on: authorization decisions for AI agents need to happen at three levels simultaneously. Identity level (the usual RBAC stuff): who is the user, what roles do they have, what resources are they allowed to access? Intent level : what was the original request, and does the current action fall within the reasonable scope of that request? Context level : given what the agent already retrieved in this session, does returning this additional piece of information create a combination that violates any privacy or least-privilege principle? Most systems only do level one. Level two and three require something fundamentally different. For intent-level authorization, I've been experimenting with attaching a structured intent object to every agent session and wiring it into Spring AI's tool execution pipeline. Java 24 records are a clean fit for this because they're immutable by default, which matters when you're passing session state across tool calls and you really don't want something accidentally mutating the intent mid-chain. Here's a simplified version of how this looks in practice: // AgentIntent.java public record AgentIntent( String userId, String userRole, String originalQuery, List<String> permittedResourceTypes, SensitivityLevel sensitivityCeiling, String sessionId, List<String> retrievedResourceIds ) { public AgentIntent withRetrievedResource(String resourceId) { var updated = new ArrayList<>(retrievedResourceIds); updated.add(resourceId); return new AgentIntent( userId, userRole, originalQuery, permittedResourceTypes, sensitivityCeiling, sessionId, List.copyOf(updated) ); } } // SensitivityLevel.java public enum SensitivityLevel { PUBLIC(0), INTERNAL(1), CONFIDENTIAL(2), RESTRICTED(3); private final int rank; SensitivityLevel(int rank) { this.rank = rank; } public boolean exceeds(SensitivityLevel ceiling) { return this.rank > ceiling.rank; } } // AtsRolePermissions.java public final class AtsRolePermissions { private static final Map<String, List<String>> PERMITTED_RESOURCES = Map.of( "coordinator", List.of("job_requisition", "candidate_profile", "interview_schedule"), "recruiter", List.of("job_requisition", "candidate_profile", "interview_schedule", "scorecard", "pipeline_report"), "hiring_manager", List.of("job_requisition", "candidate_profile", "scorecard", "headcount_plan") ); public static List<String> forRole(String role) { return PERMITTED_RESOURCES.getOrDefault(role, List.of()); } public static SensitivityLevel ceilingForRole(String role) { return "hiring_manager".equals(role) ? SensitivityLevel.CONFIDENTIAL : SensitivityLevel.INTERNAL; } } // AgentAuthorizationService.java @Service public class AgentAuthorizationService { p...