Decision Tokens: Why 'Allowed' and 'Here's the Secret' Must Be Two Different Answers
9 min read Fullmakt Team
- security
- agents
- credentials
- governance
- traceability
- authentication
Ask most teams how their AI agent gets a credential and you’ll hear a single verb: “it fetches the key.” One call goes out, one call comes back, and somewhere inside that round trip a policy check supposedly happened. The problem is that word “somewhere.” When permission and secret arrive in the same response, from the same code path, at the same instant, you cannot later prove which one actually gated the other — and you cannot revoke the first without also breaking the mechanism that would have delivered the second.
The failure: one answer pretending to be two questions
“Is this agent allowed to do this, right now?” and “here is the credential to do it” are different questions with different lifetimes. The first should be re-evaluated on every single call, against current policy and current context. The second, once issued, tends to keep working until it expires or someone remembers to rotate it — which in most scoped-credential setups is minutes or hours later, not milliseconds.
Collapse them into one step and you inherit the worse of the two lifetimes. A policy engine that decides “yes, allow this MCP tool call” and, in the same breath, hands back a live API key has just made an authorization decision that lives as long as that key does. Revoke the agent’s access a minute later and the key it already holds doesn’t care — it’s plaintext, sitting in whatever process memory or log line captured the response, and it will keep working against the upstream API until it naturally expires. The audit trail shows one event — “credential dispensed” — where it needed to show two: a decision, and a use of that decision.
Why this is an agentic-AI-gone-wrong pattern, not a hypothetical
This is exactly the shape of failure behind several patterns we’ve covered before, wearing a credentials costume instead of a prompt-injection one. An offboarded agent that keeps working past its revocation date is usually not bypassing a permission check — it’s holding a secret that was minted before the revocation and never had a reason to stop working on its own. A confused-deputy MCP tool tricked into an unintended action is more dangerous specifically because the credential it’s holding was scoped for “whatever this session was originally allowed,” not for the one call it’s currently misusing. In both cases, separating “was this specific call allowed” from “what secret does the agent hold” would have shrunk the blast radius from “until someone notices” to “until the next call.”
The gap gets wider across a chain. In an A2A delegation, Agent A’s credential for calling Agent B was often minted once, at the start of a task, then reused for every skill invocation inside it. If policy changes mid-task — a budget cap is hit, a human revokes approval, a sensitive-field rule tightens — none of that reaches a credential that was already handed over and is now just a bearer secret nobody is watching anymore.
Why “rotate keys more often” doesn’t fix it
Faster rotation shrinks the window, but it doesn’t close the structural gap: the credential and the decision to grant it are still minted together, so a short-lived key is still a standing answer to “is this allowed,” even at a five-minute TTL. It also pushes the cost onto every downstream system that has to handle more frequent key churn, and it does nothing for traceability — a log line that says “key issued at 14:02:03” still can’t tell an incident reviewer whether the specific action the agent took at 14:02:47 was individually evaluated against policy, or just rode on a credential that had already cleared the gate forty-four seconds earlier for something else entirely.
Where the fix actually has to live
- Split the policy engine from the secret store. The component that decides “is this principal allowed to call this tool with these arguments, right now” should not be the same component that holds or releases plaintext credentials. They can run in the same process today and split later, but the interface between them has to exist from day one, or the split never happens.
- Make the decision a token, not a side effect. The output of a policy check should be a small, signed, single-purpose artifact — who’s asking, for what tool, with what arguments (hashed, not embedded), issued when, expiring when — not a credential. That token proves a decision was made; it is not itself a capability.
- Give the decision token a shorter life than the credential it unlocks. Seconds, not minutes. A decision token that’s still valid a minute later is a decision that’s gone stale — policy may have changed, the human approval it depended on may have been withdrawn, the budget it was checked against may be exhausted. A dispenser that still honors a sixty-second-old decision is quietly re-trusting a moment that’s already passed.
- Only dispense the secret against a currently-valid token, checked at the moment of dispensing. The component holding the actual API key, vault entry, or OAuth secret should verify the decision token’s signature, expiry, and that it matches this exact principal and this exact tool call — every single time, with no cache of “this agent was good five minutes ago.”
- Log the decision and the dispensing as two separate, linked events. An incident review needs to answer “was this allowed” and “was the credential used” independently. If dispensing a secret is the only recorded event, a policy engine that never actually got consulted looks identical, after the fact, to one that approved every call correctly.
The business case: this is how Fullmakt’s broker is actually built
This isn’t a proposal — it’s the credential path Fullmakt already runs in split-mode deployments, and the reason the architecture is split that way in the first place:
- A policy decision point (PDP) and a credential dispenser (CDP) are separate components, and the CDP will not release a secret without a token the PDP signed for that specific call — there is no path from “agent asks for a key” straight to “agent gets a key” that skips the policy check, because the dispenser has nothing to check against except a signed decision.
- The decision token is scoped tightly and expires fast — principal, collection, tool, a hash of the arguments, and a short time-to-live measured in seconds, not the credential’s own lifetime. A stale token simply fails verification at the dispenser; there’s no fallback to “let it through anyway.”
- The agent itself never receives a raw secret to hold. Credential
references —
vault://,env://, and other backend-specific schemes — are what flow through variables and tool arguments. Resolution to plaintext happens only at the moment of execution, gated by that decision token, and the resolved value never round-trips back to the model. A poisoned tool description or a manipulated argument can’t exfiltrate a secret the agent was never given. - Every resolved reference — never the value — lands in the audit trail, tied to the call that consumed it. That’s the two-event record the fix above calls for: a policy decision and a credential use, reconstructable as a connected pair instead of one opaque “key fetched” line.
- Revocation takes effect on the next call, not on the next key rotation, because there is no standing key for revocation to race against — only a short-lived decision that has to be re-earned every time.
The commercial argument is the same one behind every credential-governance gap we’ve written about: teams building agentic AI directly against raw API keys are re-implementing an authorization boundary inside every tool and every agent, inconsistently, under time pressure. Fullmakt puts that boundary in one place — between the decision and the secret — so “was this allowed” and “was this used” are always two separate, auditable answers, not one credential doing double duty.
FAQ
Isn’t a short-lived API key basically the same thing as a decision token? No — a short-lived key is still a capability an agent can hold and reuse until it expires. A decision token isn’t a capability at all; it’s proof that a specific call was evaluated, and it unlocks a secret only at the dispenser, which re-checks it every time. Shrinking a key’s TTL narrows the window; splitting decision from dispensing removes the assumption that a single check should govern more than one action.
Doesn’t this just move the problem — now you have to protect the decision token instead of the credential? A leaked decision token is worth far less than a leaked credential: it’s scoped to one principal, one tool, one argument hash, and it expires in seconds, so by the time anyone could misuse it, it’s already invalid. A leaked standing credential keeps working until someone notices and rotates it.
Why not just re-check policy before every call and skip the token entirely? That works if the policy engine and the credential store are the same trusted component making the same call synchronously. It stops working the moment you want them to be separate services, separate deployments, or separate trust boundaries — which is exactly when you need a portable, verifiable proof of “this was decided,” instead of an implicit assumption that whoever asked for the secret must have already checked.
Does this slow every agent call down with an extra round trip? It adds one, but the decision token is cheap to verify — a signature check against a shared key — and the alternative is a system where you can’t distinguish a properly gated call from one that skipped the gate. The latency cost buys you the one thing rotation and scoping alone can’t: proof that the decision and the use of it are separately auditable.
What should show up in an audit log if this is working correctly? Two linked entries per action: a decision record (principal, tool, arguments hash, allow/deny, timestamp) and a dispensing record (which reference was resolved, for which decision, at what time) — not a single “credential retrieved” line with the assumption that a policy check must have happened somewhere upstream.
A login handshake proves an agent is who it says it is. A scoped credential limits what it can do once it’s holding one. Neither one answers whether this specific call, right now was actually checked against policy before the secret that makes it possible left the vault — that’s a question only a decision, verified at the moment of dispensing, can answer.