# Give your agent a valet key

Source: https://tenuo.ai/blog/give-your-agent-a-valet-key.html

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 Zenity found, 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 almost every agent deployed today makes that same choice, quite possibly including yours.

## What happened

[Zenity Labs](https://labs.zenity.io/post/salesbleed-0-click-data-exfiltration-on-agentforce) found three flaws in Agentforce, Salesforce's AI agent platform. They reported them on June 1, and said they had verified the fixes by September 21. The research is good and the writeup is worth reading in full.

1. An attacker submits a public Web-to-Lead form, like the one behind a company’s “contact sales” page. The fields contain instructions written for the agent rather than the sales team. This is indirect prompt injection: untrusted data enters the agent’s context and tries to redirect its behaviour. Nothing happens yet. The poisoned lead simply waits in the CRM.
2. Later, a sales rep asks Agentforce to check their latest leads. The agent reads the poisoned record and follows its instructions.
3. The agent queries the Accounts table and pulls company names and deal sizes.
4. It tucks that data into the hostname of an image URL on the attacker's server. The browser goes to fetch the image, and the DNS lookup alone delivers the data.

Salesforce’s Trusted URLs control should have blocked the final step, but URL-parsing flaws allowed the attacker-controlled hostname through. Zenity also found a second exfiltration path through Slack link previews and a [separate issue](https://labs.zenity.io/post/salesbleed-hijacking-agentforce-in-slack-for-anonymous-phishing) that allowed Agentforce to post unattributed Slack messages. Salesforce patched those vulnerabilities.

The rep asked about their own leads, and the agent's authority covered the entire Accounts table.

The gap between those two facts is the whole incident, and no amount of patching closes it, because it isn't a bug.

## The authorization layer did what it was configured to do

Trace SalesBleed through the authorization layer and the calls remain within Agentforce’s configured permissions. The rep could invoke the agent. The agent could read leads. The agent could also query Accounts.

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.

Ordinary software is deterministic. Its call graphs were static: the same input took the same path and made the same calls, every time. If you knew what the code did, you could trust it with access.

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, and the policies behind them described the role. 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. There's no one to hold to account the way the old model assumed. 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 is accountable?

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 do not 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 the full range of tasks it may 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, or limit how the car may be driven. The valet is the same person. The key is scoped to the task at hand.

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 is 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. The agent could then receive a warrant like this:

```python
warrant = (Warrant.mint_builder()
    .capability("query_records", object=Exact("Lead"), id=Exact("00Q5e000001AbCdEAO"))
    .holder(agent_key.public_key)
    .ttl(60)                      # seconds
    .mint(org_root_key))          # signed by your platform when the request arrives
```

The warrant covers one operation on one Lead record. Accounts are not in it, so the agent cannot query Accounts no matter what the poisoned record says. The 60-second lifetime limits how long the warrant can be reused after the task. In this attack, however, scope is the critical control.

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 key delegated which authority to the next. The receiving tool can verify the chain locally without calling a central authorization server.

Now run the attack again. The poisoned lead still lands. The agent still reads it. The model may still follow the injected instructions.

![SalesBleed replayed. With a table-scoped tool grant, the poisoned lead is read, the Accounts query is allowed, and deal data leaves. With a task-scoped warrant for one lead and 60 seconds, the same Accounts query is denied and logged, and nothing leaves.](/images/salesbleed-replay.png)

But when it tries to query Accounts, the Tenuo authorizer rejects the call because the warrant does not cover it. The failed attempt is recorded against the task. The injection succeeds at influencing the model, but it cannot cross the tool boundary.

## What this doesn't fix

We want to be upfront about what Tenuo doesn't do.

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.

Task-scoped authority also requires a different development model. Most systems authorize a user or service role and stop there. Tenuo adds another step: derive the narrowest practical grant for the current task.

Our job is to make that grant cheap enough and invisible enough that developers use it by default.

## 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](https://modelcontextprotocol.io) servers, [A2A](https://a2a-protocol.org) handoffs, workflow engines, and organizational boundaries. A feature implemented inside one vendor’s platform stops at that vendor’s edge.

The OAuth standard doesn't give a token holder a way to narrow a token on its own before handing it on. [OAuth Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) can issue a narrower token, but only by calling the authorization server at every hop. That's a poor fit for how agents work. They 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.

We have an ongoing IETF Internet-Draft, [Attenuating Authorization Tokens for Agentic Delegation Chains](https://datatracker.ietf.org/doc/draft-niyikiza-oauth-attenuating-agent-tokens/), to address 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. If you work on OAuth, agent frameworks, or MCP, read it and tell us where it's wrong. The [plain-English summary](https://tenuo.ai/aat-ietf-summary) is a faster way in.

Security agencies are moving in the same direction. [Joint Five Eyes guidance](https://www.cisa.gov/news-events/news/cisa-us-and-international-partners-release-guide-secure-adoption-agentic-ai) recommends strict privilege controls for agentic systems and warns organizations not to give agents broad or unrestricted access. [Separate guidance](https://www.cyber.gov.au/business-government/secure-design/artificial-intelligence/opportunities-for-ai-in-cyber-defence) from Australia’s ACSC recommends dynamic tokens, narrowly scoped autonomous actions, clear privilege boundaries, and traceable AI-assisted actions.

The EU AI Act creates related human-oversight and record-keeping requirements for systems that fall within its high-risk categories. Task-scoped authority and delegation records can support the controls and evidence needed for those processes. They do not establish compliance on their own. We explain the connection in more detail in our guide to [Tenuo and the EU AI Act](https://tenuo.ai/eu-act).

## Where to start

The fastest way in is the [quickstart](https://tenuo.ai/quickstart/). It 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](https://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.

The related standards work is documented in our [IETF Internet-Draft](https://datatracker.ietf.org/doc/draft-niyikiza-oauth-attenuating-agent-tokens/).

Will task-scoped authority become the default way agents get permissions? We don't know. We're fairly sure the next SalesBleed will look a lot like this one: a new injection or exfiltration path wrapped around an agent with a broad standing grant.

If you think we've got this wrong, [open an issue](https://github.com/tenuo-ai/tenuo/issues) or reach out via the [IETF OAuth mailing list](https://www.ietf.org/mailman/listinfo/oauth).
