Give your agent a valet key
Tenuo Engineering ·October 2, 2026
Give your agent a valet key
Tenuo gives AI agents authority scoped to the task they’re doing right now, instead of everything their role allows. Every tool call carries a short-lived signed grant that can only get narrower as it passes between agents. It’s open source, it works with MCP, LangChain, CrewAI and more, and the quickstart takes about five minutes.
When you hand a valet your regular car key, you hand over everything the car can do: top speed, the trunk, the glovebox, and the registration sitting inside it. You accept that because you trust the person and because, if they drive to Reno instead of the parking garage, there’s a police report and a lawsuit waiting at the other end.
That trade works well enough for people. It works a lot less well for agents, and a recent attack shows why.
On September 24, Zenity Labs disclosed SalesBleed, a zero-click attack chain that tricked Salesforce Agentforce into leaking CRM data to an attacker. Salesforce has since fixed the bugs, so this isn’t really a post about Salesforce. What we want to look at is the design choice underneath those bugs, the one that turned a few parsing flaws into a data leak, because many agent deployments make that same choice.
What happened
Zenity Labs found the flaws, and their writeup is worth reading in full. Here’s the short version.
An attacker fills in the “contact sales” form on a company’s website. The fields aren’t written for the sales team. They’re written for the agent. The poisoned lead sits in the CRM and waits.
Later, a rep asks Agentforce to check their latest leads. The agent reads the poisoned one, queries the Accounts table, and stuffs company names and deal sizes into the hostname of an image URL on the attacker’s server. The browser fetches the image, and the DNS lookup alone carries the data out.
Salesforce patched the URL-parsing bug that let this through. Now look at what didn’t break. The rep asked about their own leads, and the agent could read the whole Accounts table.
Patching closed this exfiltration path. The broader authorization gap remains wherever an agent can read beyond the task it was given. That should worry you, because your agent is probably set up the same way.
The authorization layer did what it was configured to do
Trace SalesBleed through the authorization layer and the calls stay within Agentforce’s configured permissions. The rep could invoke the agent. The agent could read leads. The agent could also query Accounts.
Every authorization check passed before the data crossed the boundary.
What the authorization layer did not know was why this call was happening. The rep’s intent lived in the chat, but no machine-verifiable version of that intent reached the system that queried the database.
We call this shadow delegation: the agent receives a task, but the authority it exercises does not carry a verifiable description of that task. When the poisoned lead started issuing instructions, the agent could act with everything its role allowed. The attacker did not need to steal credentials. They got the agent to use its own permissions on their behalf.
It gets worse as agents hand work to other agents and tools. At each hop, the next one acts either with the caller’s permissions or with its own standing permissions, but neither necessarily says what this particular task is for. The system at the end of the chain, the one that reads the database or moves the money, makes the most consequential decision with the least context.
Agents break the half you weren’t watching
To see what agents break, let’s start with why roles worked. Role-based access control isn’t a bad idea that everyone adopted by mistake. It worked for decades for two reasons: the software was predictable, and people could be held accountable.
Traditional applications execute code paths engineers define in advance. Agents choose tools and construct a path at runtime, partly in response to data they encounter.
The role was the unit of trust. A new hire on the finance team got a badge for the finance floor and access to the finance systems. You trusted the person not to walk out with the files they could reach.
Underneath it all was accountability. If they did walk out with sensitive files, there was a name, an HR record, and a legal system waiting. Broad access survived not because it was narrow, but because deterrence quietly did most of the work.
RBAC never actually constrained what a person could do within the role. We constrained what they would do by making sure misuse had consequences.
Agents are human-like in that they reason, pick tools, and work out how to get to a goal. That’s also why their call graph isn’t static. A probabilistic model picks the path at runtime, and something it reads, like a lead from a web form, can change it.
But they’re nothing like people in the part that made broad grants survivable. When an agent reads a poisoned lead and sends your pipeline to an attacker, the vendor didn’t do it. The rep who asked about their leads didn’t do it. So who do you fire?
The control you were actually relying on, accountability, has nothing to attach to. And the control you thought you were relying on, the role, was never doing that work.
This is also why “just make the model more resistant to injection” isn’t a sufficient answer, even though it’s a worthwhile one. You’re asking a probabilistic system to be reliably unpersuadable, and then staking the contents of your CRM on it. The model is allowed to be fooled. What it can reach while fooled is the part you get to decide.
The common fixes don’t close this gap on their own. Passing the user’s credentials gives the agent the user’s standing access. Giving the agent its own role means that role must cover every task it might perform. Issuing a fresh token at every hop helps only if the token is narrower.
A policy engine can evaluate task context, but only if every caller supplies that context consistently. In most agent systems, the task still lives in a prompt rather than in a verifiable authorization artifact. And making the agent read-only would not have stopped SalesBleed. The sensitive action was a read.
Identity and roles answer who is calling and what they may be eligible to do. The question we should be asking is: what authority was granted for this task?
Car manufacturers solved this a long time ago
Back to our valet. Car manufacturers addressed this problem decades ago, and they did not do it by vetting valets more carefully.
A valet key opens the door and starts the engine, but it won’t open the trunk or the glovebox. The valet is the same person. The key is scoped to the task at hand.

Dodge ships the Hellcat with two keys. The red one unlocks all 707 hp; the black one caps it at 500. Guess which one the valet gets.
Hire the same person as your chauffeur and they get a different key: highway speeds, longer trips, and access to the trunk for your luggage. Same person, different task, different authority.
That is the shift. Authority is attached to the task, not only to the identity holding it.
RBAC describes what a principal may generally do. Task-scoped authority narrows that standing access to what this request may do, with which resources, and for how long.
For people, standing access was good enough, because the legal system backstopped the gap between “generally allowed” and “actually did.” The legal system can’t backstop agents, so the grant has to be the control. The risk stops being a judgment about the actor and becomes a property of the key, one the enforcement point can check.
Tenuo is built on that idea. Every call carries three things: what it’s for (authority granted for this one request), how it got here (signed at every delegation hop), and who’s holding it (proof that the caller is the one it was granted to, so a stolen copy is useless).
We call the key a warrant. It’s a small signed token, minted for a task and carried with the agent’s tool calls. Assume a trusted orchestration step has resolved “my latest lead” to a specific record. Here’s the same attack hitting a task-scoped warrant instead, with Tenuo blocking the exfiltration:
# pip install tenuo
import time
from tenuo import Authorizer, Exact, SigningKey, Warrantplatform_key = SigningKey.generate() # your platform mints warrants
agent_key = SigningKey.generate() # the agent proves it holds them
The rep asked about one lead, so the warrant covers one lead for 60 seconds.
warrant = (Warrant.mint_builder()
.capability("query_records", object=Exact("Lead"), id=Exact("00Q5e000001AbCdEAO"))
.holder(agent_key.public_key)
.ttl(60)
.mint(platform_key))authorizer = Authorizer(trusted_roots=[platform_key.public_key])
def call(tool, args):
pop = warrant.sign(agent_key, tool, args, int(time.time()))
try:
authorizer.authorize(warrant, tool, args, bytes(pop))
print("ALLOWED", args)
except Exception as err:
print("DENIED ", args, f"({type(err)._name_})")
call("query_records", {"object": "Lead", "id": "00Q5e000001AbCdEAO"}) # the task
call("query_records", {"object": "Account", "id": "*"}) # the injection
ALLOWED {'object': 'Lead', 'id': '00Q5e000001AbCdEAO'}
DENIED {'object': 'Account', 'id': '*'} (ConstraintViolation)The warrant covers one operation on one Lead record. Accounts are not in it, so the agent can’t query Accounts no matter what the poisoned record says. The 60-second lifetime limits how long the warrant can be reused after the task, but in this attack, scope is the control that matters.
If the agent delegates work to a sub-agent or tool, it can pass along an equal or narrower slice of that authority, never a broader one. Each hop is signed, producing a verifiable record of which holder delegated which authority to the next. The receiving tool can verify the chain locally without calling a central authorization server.
So the poisoned lead still lands. The agent still reads it. The model may still follow the injected instructions. But the Accounts query is rejected at the tool boundary. The denial is also available as a signed receipt tied to the warrant presented for the task.

Want to try it on a misbehaving agent without installing anything? The delegation lab lets you stop one in your browser.
What this doesn’t fix
Tenuo does not stop prompt injection. The model can still be fooled. A warrant limits which resources and actions it can reach when that happens.
It also does not make filters or authorization policies bug-free. Salesforce’s URL control had parsing flaws, and a warrant can also be scoped incorrectly.
What changes is the blast radius. A failure can expose only the data and actions made available to that task, rather than everything attached to the agent’s standing role.
Pick one task your agent runs a lot, scope a key to it, and see what breaks. And if you think we’ve got this wrong, open an issue or poke holes in the IETF draft. We’d like to hear it.
Further reading
Why this needs to be a standard. If task authority travels with a call, every system that enforces it needs a shared way to interpret it. Agent chains cross MCP servers, A2A handoffs, workflow engines, and organizational boundaries. A feature built inside one vendor’s platform stops at that vendor’s edge.
OAuth doesn’t give a token holder a way to narrow a token on its own before handing it on. OAuth Token Exchange can issue a narrower token, but only by calling the authorization server at every hop. Agents spin up sub-agents on the fly, hand work back and forth in loops, and call tools run by other teams. Waiting on a central server at every handoff is slow and fragile, so the easy path is to pass the original token along. That’s shadow delegation again.
Our IETF Internet-Draft, Attenuating Authorization Tokens for Agentic Delegation Chains, addresses that gap. It defines task-scoped tokens the holder can narrow offline, proof of possession at every hop, and a chain any verifier can check without calling home. The plain-English summary is a faster way in, and the IETF OAuth mailing list is the place to argue with it.
Where regulators are landing. Joint Five Eyes guidance tells organizations to put agents under strict privilege controls and not to give them broad or unrestricted access. Australia’s ACSC goes further: dynamic tokens, narrowly scoped autonomous actions, clear privilege boundaries, traceable AI-assisted actions. That’s close to a spec for a warrant. The EU AI Act asks high-risk systems for human oversight and record-keeping, and a signed record of who delegated what, for which task, covers a real part of that.
Where to start. The quickstart takes about five minutes: install the package, run a copy-paste example, and pick the integration for your framework. For a real agent, start with one task it runs often, write a warrant scoped to that task, and widen it only when a legitimate call needs more.
The open source implementation is Apache 2.0 at github.com/tenuo-ai/tenuo, with SDKs for Python and TypeScript and integrations for MCP, A2A, LangChain, LangGraph, CrewAI, Temporal, Google ADK, OpenAI, AutoGen, and FastAPI.
Comic: “Authorization” by Randall Munroe, xkcd, CC BY-NC 2.5.
Newsletter
Keep up with Tenuo Engineering
Get each new article in your inbox.
Email address Subscribe
Confirm by email. Unsubscribe at any time.



