The authorization layer for AI agents · MIT
Your agent logs in as you.
It shouldn't get to be you.
Policy, approvals, and receipts on every agent tool call — dependency-free, framework-neutral, no SaaS.
$ agent reads an email — injected instructions inside
→ model obeys: email.forward → [email protected]
✗ BLOCKED before the tool ran — deny:email.forward (exfiltration)
? email.delete? held for a human — denied
§ every decision sealed on the receipt chain
✓ chain verifies — doctored log detected
→ the model was compromised. the policy didn't care.
Works with the stack you already run
- Vercel AI SDK & Eve
- Cloudflare Agents & Workers
- Auth0 Token Vault & CIBA
- Supabase Token store
Run it yourself — no API keys, no network
An injected agent tries to exfiltrate your data. Policy says no.
The tools are wrapped with nominee.guard(). The model obeys the
injection anyway — models do — and the deny rule fires
before the tool runs. Every attempt, allowed or refused, is
sealed into a signed, tamper-evident receipt chain. One command:
2. the model obeys the injection and tries to exfiltrate
✓ BLOCKED before the tool ran — deny:email.forward (exfiltration)
5. the receipt chain (signed, tamper-evident)
#0 policy.decision email.read allow 5493c2c54cd5
#1 policy.decision email.forward deny ca6a069febdb
#2 policy.decision email.delete ask d2fe628a5202
#4 approval.resolved email.delete denied 2b0ac4aa3ad8
#5 policy.decision email.forward allow fd17436d92c0
✓ chain verifies: 6 receipts intact
✓ doctored log detected — broken at #1
cd examples/prompt-injection-blocked && node run.mjs —
source ↗
Tokens, the supporting act — settled live
Same agent. One outlives its token.
Two agents call a protected API across a session longer than the token
lives (~8s here). Left grabs a token once and reuses it. Right calls
nominee.token() each time. The real nominee
package runs in your browser — open the Network tab and watch the refresh.
That's the expiry half. Add refresh-token rotation + concurrency and naive code fails 7/8 while nominee gets 8/8 — a runnable proof, no mocks that cheat: examples/token-refresh-correctness.
Start on your DB → Auth0 → Nango. Your agent code never changes — only the strategy line does. The Passport.js of agent auth, scoped to the no-lock-in tail:
const nominee = new Nominee({
strategy: ({ user, connection }) => db.refreshToken(user, connection), // zero signup
})
// everything below is identical, whatever the strategy ↓
const token = await nominee.token({ user, connection: 'github' })
const nominee = new Nominee({
strategy: OAuth2({ connections: { github: { tokenEndpoint, clientId, refreshToken } } }), // bring your own OAuth
})
// everything below is identical, whatever the strategy ↓
const token = await nominee.token({ user, connection: 'github' })
const nominee = new Nominee({
strategy: Supabase({ url, key, connections: { github: { tokenEndpoint, clientId } } }), // tokens stored in Supabase
})
// everything below is identical, whatever the strategy ↓
const token = await nominee.token({ user, connection: 'github' })
const nominee = new Nominee({
strategy: Auth0({ domain, clientId, clientSecret, subjectToken, ciba }), // managed Token Vault + CIBA
})
// everything below is identical, whatever the strategy ↓
const token = await nominee.token({ user, connection: 'github' })
The race above runs the oauth2 line. Want the auth0 line for real? → watch a live agent act on your GitHub, after your approval.
How it works
Checked against policy. Held for a human. On the record.
You're the principal; your agent is the nominee, acting for you within a scope. nominee checks every tool call against your policy, holds the risky ones for a person, and seals every decision — including refusals — into a tamper-evident record. No SaaS in the middle.
-
¶
Policy on every call
Declarative allow / deny / ask rules — glob patterns, argument-level conditions, call budgets. A deny fires before the tool runs; the model can't talk its way past it.
-
⏸
Held for a countersignature
Gate the risky calls behind a real person. The agent pauses, a human approves from a phone or a UI, then it resumes — no stale token on the other side.
-
§
On the register
Every decision, approval, and token grant is a hash-chained, optionally signed receipt: you → agent → service, who authorized what, and when. Edit or delete a record and verification breaks.
Install and go
A policy, not a platform. No service to sign up for.
Give nominee a policy — ordered allow / deny / ask rules over your tools. Approvals, receipts, and fresh tokens ride on top, same API wherever your agent runs.
import { Nominee, allow, deny, ask } from 'nominee'
// a boolean flag (needsApproval: true) is one Y/N prompt for everything.
// a policy is allow / deny / ask — per tool, argument-aware:
const nominee = new Nominee({
policy: [
allow('email.read'),
deny('email.forward', { reason: 'exfiltration' }),
ask('email.delete'), // a human decides, every time
],
})
// one line — every call checked before the tool runs
const tools = nominee.guard({ 'email.read': readEmail, 'email.forward': forwardEmail }, { user: 'alice' })
import { guardTools } from 'nominee-ai'
// wrap your existing AI SDK tools — the key is the name your policy matches
const result = await generateText({
model,
tools: guardTools(nominee, { searchEmail, forwardEmail, mergePr }, {
user: session.userId,
}),
})
// per-tool config (connection tokens, approval: true) → nomineeTool()
import { Nominee } from 'nominee'
import { Auth0 } from 'nominee-auth0'
// same API — swap the strategy for managed Token Vault + phone approval
const nominee = new Nominee({
strategy: Auth0({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
subjectToken: ({ user }) => store.getRefreshToken(user),
ciba: { bindingMessage: (req) => `Approve: ${req.action}` }, // phone approval
}),
})
Human in the loop
Hold the risky moves until a person signs.
An ask rule in your policy holds deletes, payments, or outbound
mail for a real approval — deny means deny. Use the built-in engine, or a
native flow like Auth0 CIBA that pings the user's phone. The agent pauses,
then resumes exactly where it left off.
repo.delete
Delete repository alice/old-project
Where nominee sits
A thin, neutral seam — not another platform.
Your framework runs the agent, but authorization is left as a Y/N flag — or nothing at all. nominee is the layer in between: every tool call is checked against your policy before it runs, risky ones pause for a human, every decision lands on the receipt chain — and your tools and vault stay swappable underneath.
authorize({ tool, input, user })execute(input)When you don't need nominee
- A read-only agent with no authority worth guarding.
- Your platform's native permission system covers you end-to-end and you're happy inside it.
- You want one fully-managed vendor for tools + auth + policy — use Arcade or Composio directly.
Reach for nominee when you want policy + approvals + receipts that are framework-neutral, no-SaaS, and bring-your-own-everything — the same policy wherever your agent runs.
Packages
One authorization layer, wherever your agent runs.
nominee-ai
guardTools wraps your AI SDK tools in one line; nomineeTool for per-tool config. Runs on Cloudflare Agents too.
nominee-supabase
Store provider tokens in Supabase; nominee reads and refreshes them. Zero deps.
nominee-auth0
Auth0 Token Vault for federated tokens, and CIBA approvals on the user's phone.
FAQ
The questions people actually ask.
Isn't this just an if-statement with extra steps?
The check itself, yes. What you're buying is everything around it: ask-rules that escalate to a human instead of silently failing, budgets that let 20 calls through and ask on the 21st, delegation that a sub-agent can only narrow, a tamper-evident receipt of every decision, and the same policy enforced whether your agent runs on the Vercel AI SDK, Eve, or a bare function — one enforcement point instead of scattered if-statements per tool, per framework.
Can the model talk its way past a deny?
No. The model never sees the policy — it only ever gets the tools you handed it
via nominee.guard(). A deny rule throws
PolicyDeniedError inside the wrapper, before your tool's own code
runs. There's no prompt for the model to argue with; the call physically doesn't
happen.
Do receipts store my data?
Not by default. Tool inputs are recorded as inputHash — a SHA-256 of
the canonical JSON — so you can prove what an approver saw without writing user
data into the log. Pass receipts: { input: 'raw' } if you want the
full input on the record instead, or 'none' to skip it entirely.
Can receipts be forged?
Tamper-evident, not non-repudiation — say that plainly. Without a signing key,
the chain is publicly re-computable: anyone can verify it wasn't edited, but
anyone with write access to the whole log could also rewrite it consistently.
Pass an HMAC key and only key-holders can produce valid hashes —
that's the guarantee to reach for once receipts leave your own process.
How is this different from Arcade, Composio, or Vercel Connect?
Those are hosted platforms for tool-calling and connectors — good products, but SaaS: per-call billing, and the policy brain lives on their side. nominee is an MIT, in-process, framework-neutral policy layer that can sit on top of any of them (or your own DB, OAuth2, or Auth0) as the token strategy. Bring your own vault; keep the policy, approvals, and receipts local.
Does nominee replace my auth provider (Auth0, Clerk, WorkOS)?
No — authentication (who is the user) is a separate, solved problem. nominee is the layer above it, deciding what an already-authenticated agent may do as that user. It composes with whatever you already use; Auth0 is one optional strategy among several, not a requirement.
What happens if I don't configure a policy?
Everything is allowed — and still receipted. nominee doesn't restrict anything until you add rules, so you can adopt it incrementally: wrap your tools first, watch the receipt chain to see what your agent actually does, then tighten the policy once you know.
When don't I need nominee?
A read-only agent with no authority worth guarding. Your platform's native permission system already covers you end-to-end. Or you want one fully-managed vendor for tools, auth, and policy together — use Arcade or Composio directly.
Give your agent a nominee.
Open source, MIT, zero-dependency core. It works the moment you install it.
▶ Watch a real agent act on your GitHub, after your approval →