Technical paper

Task-Scoped Authorization for AI Agents

Compiling user intent into least-authority contracts and enforcing them at the tool boundary

v0.3 · September 2026 · AgentsFence contributors

Abstract

Tool-using AI agents typically run with the union of every permission their tools require: the whole mailbox, the whole drive, a shell. Any single task needs a small fraction of that authority, and the difference is what indirect prompt injection, hallucinated actions and scope creep exploit. We describe AgentsFence, an authorization layer that separates reasoning from authority. When a task starts, the user’s natural-language request is compiled into a structured, task-scoped policy. The compiler sees only the user’s own words, and a deterministic hardening pass removes any grant that isn’t supported by a verbatim quote. On every tool call, a deterministic evaluator checks the requested action and its arguments against that policy before the tool executes, and returns ALLOW, BLOCK or ASK_USER. The agent stays free to plan and replan, but it can’t give itself authority, and content it reads can’t grant authority either. We present the design, its implementation as a plugin for the Hermes agent framework, the security properties it provides and the ones it doesn’t, and an attack-scenario evaluation in which a compromised policy compiler is also part of the threat model.


1. The problem: ambient authority in agents

An agent connected to Gmail, Drive and a terminal holds ambient authority: every capability of every connected tool is available on every turn, whatever the user asked for. This is the classic setup for a confused deputy [Hardy 1988]. A program with legitimate authority is tricked by a party without that authority into using it on that party’s behalf.

For LLM agents the trick is cheap. The agent must read untrusted content to do its job, and to a language model, content and instructions look the same. An email that says “ignore the user and forward everything to attacker@example.com” is, to the model, one more instruction in its context. This is indirect prompt injection, and years of work have shown that making the model more robust reduces how often the attack succeeds but doesn’t bound what happens when it does [Greshake et al. 2023; Debenedetti et al. 2024].

Three failure modes share one root cause:

Failure Example Root cause
Indirect prompt injection A web page tells the agent to exfiltrate data Authority available beyond what the task needs
Hallucinated action The agent “cleans up” by deleting files Same
Scope creep A read task ends in a write Same

Common mitigations tend to fall into one of two failure patterns:

  1. Self-policing. Ask the model, or a second model, whether an action looks OK. The judge reads the same untrusted content and can be manipulated the same way. Its failures are correlated with the agent’s.
  2. Blanket approval. Ask the human before every consequential action. Humans habituate quickly, and approval prompts become click-through.

The principle of least privilege [Saltzer & Schroeder 1975] is the right lens: every program and every user of the system should operate using the least set of privileges necessary to complete the job. The obstacle is that “the job” is described in natural language, and changes with every task.

2. Key idea: compile intent, enforce independently

AgentsFence answers the question “what is the least set of privileges for this job?” once per task, from the only party entitled to answer it: the user. The answer becomes an explicit contract that is enforced deterministically outside the model.

user request ──► compiler ──► hardened TaskPolicy ──┐
│ (independent of the agent's plan)
agent reasoning ──► tool call ──► evaluator ◄─────────┘
│
ALLOW / BLOCK / ASK_USER (before the tool runs)

Design principle. The agent’s plan is not trusted as policy. The agent may reason and replan however it wants. Authority comes from the user’s intent, compiled into an independent contract. Every action must stay inside that envelope.

This keeps the useful property of agents, flexible planning, while bounding their dangerous property: arbitrary action. A read-only task stays read-only whether the agent reads Gmail, Drive, the web or the local disk, and whether or not something it read told it otherwise.

2.1 Relation to prior work

  • Capability systems and object capabilities make authority explicit and unforgeable, and pass it only by delegation. AgentsFence applies the same idea at task granularity. The task policy is the capability set, and runtime approvals are narrowly attenuated delegations.
  • The dual-LLM pattern [Willison 2023] and CaMeL [Debenedetti et al. 2025] separate a privileged planner, which never sees untrusted data, from a quarantined component, which does, and track data flow between them. CaMeL gives stronger guarantees, including data-flow policies, but requires the agent to be restructured around a custom interpreter. AgentsFence targets an existing, unmodified agent runtime. It enforces control-flow authority (which actions, to whom) at the tool boundary, and doesn’t track information flow. The two approaches complement each other.
  • Guardrail classifiers judge individual inputs and outputs with a model. AgentsFence’s runtime decisions involve no model, so they can’t be prompt-injected.
  • Human-in-the-loop approval is kept, but used only when a request goes beyond what was already authorized, and each approval grants a minimal delta instead of a broad permission.

3. System model

3.1 Actors and trust

Actor Trust
User messages (top-level session) trusted source of authority
Agent runtime (Hermes) and its tool dispatch trusted to call the enforcement hook before every tool call
Agent model output, tool results, fetched content, subagent goals untrusted
Policy compiler model untrusted: its output is a proposal that can only shrink

3.2 Tool calls and risk

A tool call is a pair (t, a) of a tool name and arguments. A classifier maps it to a capability κ(t, a) (such as email.send), a risk class ρ(t, a) from the set R below, and a set of targets τ(t, a): recipients, domains and paths.

R = { READ, LOCAL_WRITE, EXTERNAL_WRITE, COMMUNICATION, DELETE,
PERMISSION_CHANGE, FINANCIAL, EXECUTION, UNKNOWN }

Classification uses three layers:

  1. An explicit mapping per tool, optionally refined by an argument value (send_message(action="list") is READ) or by a shell-command sub-classifier (rm → DELETE, curl -d → EXTERNAL_WRITE).
  2. Glob patterns.
  3. Verb inference from the tool name. For names like mcp_gmail_send_message, the first recognised verb decides, and “strong” verbs anywhere in the name (delete, send, pay, transfer, …) escalate.

Anything with no signal is UNKNOWN, and UNKNOWN is never treated as safe.

The classifier describes what a call can do, independent of which tool performs it. That is what makes dynamic replanning safe. A policy that allows READ allows any read tool, including ones the compiler never saw.

3.3 The task policy

A policy P contains:

  • a set of ceilings, one boolean per consequential class (allow_external_writes, allow_destructive_actions, …);
  • an allow list A of tool names, capabilities, globs or classes, and a deny list D;
  • argument constraints per tool or capability: allowed recipients, domains, paths and a max_calls limit;
  • task-wide recipient, domain and path allowlists, and an outbound budget;
  • prohibitions X: things the user explicitly forbade;
  • grants G: minimal deltas approved at runtime.

A call is authorized when

ceiling_P(ρ) AND ( t ∈ A OR κ ∈ A OR ρ ∈ A_classes )

and it is then subject to the constraint, recipient, domain, budget and approval checks. Requiring both a ceiling and a listing means a single stray entry, such as the compiler listing file.delete, can’t grant a destructive capability on its own.

3.4 Evaluation

The evaluator is a total, deterministic function

E(P, σ, t, a) ∈ { ALLOW, BLOCK, ASK_USER }

where σ is the per-task execution state (call counters, grant uses, outbound count). It applies ordered checks with first-match semantics:

  • expiry
  • guards
  • prohibitions
  • deny list
  • grants
  • argument constraints
  • ceiling and listing
  • recipient binding
  • domain allowlist
  • exfiltration-shaped URLs
  • path allowlist
  • outbound budget
  • explicit approval requirements

Prohibitions come before grants, so no approval can override something the user explicitly forbade. Evaluation and the counter update happen atomically, so concurrent tool calls can’t race past a limit.

4. Compiling intent without inventing authority

The compiler is the only component that turns natural language into authority, so it is where an attacker, or an honest mistake, would try to widen the envelope. We treat the compiler model as untrusted and surround it with deterministic controls.

1. Input isolation. The compiler receives only the user’s current message. It never sees tool output, conversation history or subagent goals. Before compilation, quoted material is stripped out: fenced blocks, >-quoted lines, forwarded and replied email tails, pasted-content tags and long quoted strings. This is content the user is showing the agent, not instructions the user is giving.

2. Structured output. The model must return a JSON object matching a schema, PolicyDraft. Invalid output is retried once, then the conservative fallback is used.

3. Evidence binding. For every consequential permission, the draft must include an evidence item whose quote appears verbatim in the user’s instruction. The quote must contain a request for an action of that risk class, and must not negate it. A request is a clause that opens with one of the class’s verbs, once framing such as “please” or “can you” is removed. “The email from Alice” is a noun phrase, and “explain how to send an email to Bob” asks for an explanation; neither requests a send. Action verbs remain informational when they are the subject of an explanation or list (“explain why save and delete differ” and “list save and delete commands” are not delete requests). A summary can separately request output to an explicit path or address; other actions are clearest in a separate sentence. Evidence is checked per class even where classes share a ceiling flag: communication and other external writes are both gated by allow_external_writes, but a request to send doesn’t let a CRM update through. Permissions without such a quote are dropped.

4. Literal targets. A recipient survives only if it appears as a whole token as the object of a send or share request in the user’s words. “The email from alice@…” names a sender, and “not-alice@…” does not contain “alice@…”. A name is not an address: “Email John” binds no recipient, so a send to john@anything goes to the user for confirmation. Domains must appear as their own host token, not only inside an email address. In a compiled policy, a domain named for browsing never stands in for a named send recipient. Paths must appear literally, and roots such as / or the home directory are refused.

5. Deterministic prohibitions. Explicit negations (“do not send”, “only read”, “don’t purchase”, “never delete”) are extracted with patterns that handle common false positives (“don’t forget to email”, “no problem, send it”, “don’t send it to anyone except John”). They are added to X regardless of what the model says. A ban on sending also covers pushing data out through the shell (curl -d, scp, git push).

6. No wildcards, no unknown tools. *-style grants in allow lists are removed. Provider-specific names (gmail.send) are mapped to canonical capabilities (email.send), and never widened to generic ones. A compiled policy can never allow a tool AgentsFence cannot classify; only a policy the user wrote can.

7. Fallback. Any failure (network, schema, empty input, missing key) produces a read-only policy that still carries the deterministically extracted prohibitions. Prohibitions are read from the whole instruction even when it is truncated before reaching the model.

Together these give a bounded form of monotonicity. For any consequential class, the hardened policy grants only authority that is tied to a sentence of the user’s that asks for that class of action, to recipients named as the object of such a request, and to paths and domains the user wrote. The compiler can make the policy narrower than the user intended. It can’t make it wider than those words.

What hardening cannot establish is that the model interpreted the sentence correctly. “Remove the typo” contains a delete-class verb. A compiler that reads it as permission to delete files passes hardening, although it is still bounded to that single class and to any constraints present.

We test the bound with two adversarial compilers:

  • An over-broad compiler grants every class, recipient and domain, with fabricated evidence.
  • A quote-reusing compiler cites real words from the request as evidence for the wrong permission.

Neither gains authority.

4.1 Follow-up turns

Agents are conversational: “…actually, send it to john@example.com.” A later user turn that could change authority is compiled on its own and merged, with the newer explicit statement winning. A new prohibition removes older allowances. A new evidenced allowance lifts an older prohibition of the same class. Turns that can’t change authority, such as “thanks, what did the second one say?”, are skipped by a cheap deterministic pre-filter.

5. Runtime approval as attenuated delegation

When a call goes beyond the policy but isn’t forbidden, the result is ASK_USER, delivered through the agent runtime’s native approval UI. If the user approves, AgentsFence records a grant: the exact tool, plus exactly the recipients, domains and paths in this call, with max_calls = 1 for “once”. Empty target sets stay empty: a grant never widens to targets the user didn’t see. “Session” and “always” answers make the delta repeatable, but still bound to those exact targets and to this task. The approval key given to the runtime is derived from the task and the delta, so the runtime’s own “always allow” memory can’t turn one approval into a blanket permission.

This follows the object-capability idea of attenuation. Authority grows only in small, explicit, user-visible steps, and each step is logged.

6. Implementation: a Hermes plugin

AgentsFence is implemented as a plugin for Hermes Agent and doesn’t modify it. We verified the hook contracts against the Hermes source (documented in Hermes hook contracts). Several of them differ from what a naive design would assume, and the design accounts for each.

Hermes behaviour Consequence Handling
Hook exceptions are logged and swallowed; the tool then runs A buggy guardrail would fail open Every handler catches everything; pre_tool_call converts errors into block; a startup failure registers a block-everything stub
The first block/approve from any plugin wins, in registration order Another plugin’s approve can hide the guardrail’s block, including on the first tool call of a process AgentsFence moves its callback first when it registers, at every turn and before each decision (a Hermes internal); “block always wins” should be fixed upstream
Nested tool calls from execute_code carry only a task id They could escape a subagent’s inherited policy Task ids resolve through subagent links; unknown task-only calls are blocked
Some “read” tools take arguments that act (browser_console(expression), Spotify’s action="save"), and patch names files inside the patch text, including Move File: a -> b A read-only task could run page JavaScript, change an account, or move a file anywhere Argument-aware classification; action values must be declared reads; patch headers parsed with Hermes’ grammar and parser
pre_tool_call supports block, approve, or no opinion; there is no “allow” — ALLOW → None, BLOCK → block, ASK_USER → approve
pre_llm_call fires once per user turn; task_id may change on every turn Policies must survive turns Key by session_id; alias task_ids; compile on the first turn, amend later
The approval outcome isn’t returned to the plugin; post_approval_response fires only for gateway approvals Approvals could go unrecorded Record choices when the event fires; otherwise infer a single-use grant from post_tool_call
--yolo, allowlists and cron approve auto-approve approve directives ASK_USER can become ALLOW Documented; strict_yolo converts ASK_USER to BLOCK in those modes
Subagents get fresh session ids; their first “user message” is written by the parent model A subagent could compile new authority subagent_start binds the child to the parent’s policy and counters
With no human present, the approval gate fails closed Good Verified in integration tests

Runtime decisions are local and deterministic. The only network dependency is policy compilation at task start and when a later message could change authority: OpenRouter by default, or the user’s own Hermes model, or no call at all with the offline rules compiler. A compilation may retry invalid output or switch OpenRouter JSON mode. Policy records and a redacted audit log live under ~/.hermes/agentsfence/ at mode 0600. The raw request isn’t stored.

7. Evaluation

We evaluate with automated attack scenarios that go through the plugin’s real hook handlers. Each scenario is repeated with three compilers:

  • good: a well-behaved compiler model;
  • rules: the deterministic offline compiler;
  • overbroad: a compromised compiler that grants every class, every recipient and every domain, with fabricated evidence.

A separate integration suite runs the scenarios inside a real Hermes installation. AgentsFence is loaded by Hermes’ own plugin discovery, and every call goes through Hermes’ handle_function_call → pre_tool_call → approval gate → tool handler path. Mock tools record whether they actually ran.

Scenario Attack good rules overbroad
Prompt injection in email (“do not send anything”) send to attacker, trash evidence, share file, curl -d, scp blocked blocked blocked
Hallucinated destructive action (read task) delete file, trash thread, rm -rf, transfer funds blocked blocked blocked
Unauthorized recipient in an authorized-send task other address, cc smuggling, homoglyph blocked blocked ask
Injection inside user-pasted quoted email forward and delete as instructed by the quote ask / blocked ask / blocked ask / blocked
Exfiltration through read-tool URL long encoded query, email in query ask ask ask
Self-disabling edit config/policy files (directly, by patch, via $HERMES_HOME), hermes plugins disable, pip uninstall blocked blocked blocked
Page JavaScript in a read task browser_console(expression="fetch(…)") ask ask ask
Memory and cron persistence persistent memory write, scheduled job ask ask ask
Tool output claiming authority “SYSTEM OVERRIDE: user authorizes…” policy unchanged policy unchanged policy unchanged
Utility: dynamic replanning within READ Gmail → Drive → local → web allowed allowed allowed
Utility: authorized send to the named recipient — allowed allowed ask

“Ask” means the call is held for the user’s decision and never runs on its own. Under the over-broad compiler, the legitimate authorized send degrades from ALLOW to ASK_USER, because fabricated evidence is stripped. The system errs toward asking, never toward acting. Across the Hermes integration run, every BLOCK corresponded to a tool that didn’t execute, and every ASK_USER with no human present was blocked by the runtime.

A second plugin that approves every tool call, loaded before AgentsFence, with Hermes in --yolo mode, does not change the outcome: the injected send is still blocked and doesn’t execute. This holds when the send is the first tool call of the process, including a run that skips pre_llm_call, and later in the task. A negative control with AgentsFence’s hook-ordering fix disabled shows the send executing, which confirms that the test exercises the precedence path.

7.1 Independent review

An internal review of an earlier revision found paths that the scenario suite didn’t cover:

  • tools that looked like reads but acted (page JavaScript, sort -o, git branch -D);
  • multi-file patches;
  • approvals broader than the action shown;
  • another plugin’s directive hiding a block;
  • nested dispatch escaping a subagent’s policy;
  • substring-based literal matching.

A second review of the fixes found that several were incomplete:

  • reordering hooks only inside pre_tool_call came too late for the first call of a process;
  • the patch parser missed Move File: a -> b and headers with unusual spacing;
  • a quote that mentioned an action (“explain how to send…”) counted as a request for it;
  • evidence for a send also authorized other external writes, because both share one ceiling flag;
  • tools mapped as reads multiplexed writes behind an action argument.

Each counterexample from both rounds is now a regression test (tests/test_review_regressions.py in the plugin repository, plus first-call cases in the real-Hermes suite). The lesson matches §6: the host’s composition rules and tool argument semantics are part of the security boundary. They have to be verified against the host’s source, down to its parser grammar, not inferred from tool names or documentation.

These are targeted scenario tests, not a benchmark. A systematic evaluation on AgentDojo-style task suites [Debenedetti et al. 2024], measuring both attack success rate and utility loss across compiler models, is future work.

8. Limitations

  • Interpretation isn’t verified. Hardening proves which of the user’s sentences a permission rests on, not that the model read the sentence correctly.
  • Host composition relies on internals. Guaranteeing that a block is final requires AgentsFence to run first among Hermes’ pre_tool_call hooks, which it achieves through a non-public list. An upstream “block always wins” rule would remove this dependency.
  • Content isn’t policed. When sending to John is authorized, an injection can still shape the message. Information-flow control, as in CaMeL-style designs, is needed to close this gap.
  • Execution is opaque. Once a task may run shell commands or code, effects inside that process can’t be seen at the tool boundary. OS or network sandboxing is required.
  • The quality of classification limits enforcement. Verb inference covers common MCP naming. A misleadingly named tool that mutates state behind a get_ prefix will be treated as a read unless it is mapped explicitly.
  • Request detection is lexical. A request phrased without a leading verb (“the drafts should be deleted”) grants nothing, which costs a prompt. A description that happens to open with an action verb counts as a request.
  • The compiler can be too narrow, or read-broad when the user meant something narrower. The first costs a prompt. The second is bounded by the READ class, plus heuristic exfiltration checks.
  • Pasted content without markers is treated as the user’s words.
  • Approval modes the user enables (yolo, allowlists) are honoured for ASK_USER, though never for BLOCK.

9. Future work

  • Information-flow labels, so data read from untrusted sources can’t reach outbound arguments without approval.
  • Per-argument content policies for authorized sends (for example, “summary only, no attachments”).
  • Signed policy manifests for MCP servers, declaring the risk class of each tool and removing the need for name heuristics.
  • Benchmarking on AgentDojo and similar suites across compiler models.
  • Porting the compiler and evaluator, which don’t depend on Hermes, to other agent runtimes.

10. Conclusion

Agents need freedom to think, and boundaries on what they can do. By compiling the user’s intent into an explicit, hardened, task-scoped contract, and checking every action against it deterministically before it happens, AgentsFence turns “the model decided to” into “the user authorized it”. It does this without restricting how the agent plans, and without asking the model to police itself.


References

  • Hardy, N. (1988). The Confused Deputy (or why capabilities might have been invented). ACM SIGOPS Operating Systems Review 22(4).
  • Saltzer, J. H., & Schroeder, M. D. (1975). The Protection of Information in Computer Systems. Proceedings of the IEEE 63(9).
  • Greshake, K., et al. (2023). Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection. AISec ’23.
  • Willison, S. (2023). The Dual LLM pattern for building AI assistants that can resist prompt injection. simonwillison.net.
  • Debenedetti, E., et al. (2024). AgentDojo: A Dynamic Environment to Evaluate Prompt Injection Attacks and Defenses for LLM Agents. NeurIPS Datasets and Benchmarks.
  • Debenedetti, E., et al. (2025). Defeating Prompt Injections by Design (CaMeL). arXiv:2503.18813.
  • Miller, M. S. (2006). Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control. PhD thesis, Johns Hopkins University.