Approval is the easy part · MIT

Your agent got approval at 2:14.
It executed at 2:31 with a dead token.

Your framework can already pause a tool call — that part is solved. It cannot tell you the token expired during the pause, the arguments drifted, the permission changed, or the approval got spent twice. nominee makes the moment after "approve" correct, and leaves a receipt.

token minted at execution bound to the args a human saw spendable once
Run the proof → Add it to your agent

Does your approval come back in the same HTTP request that asked for it? Then you don't need nominee. Open-source TypeScript · no signup or API key.

pause-narrative — out-of-band approval

$ agent run --task "refund ord_42"

refund.issue({ orderId: 'ord_42', amount: 200 })

ask rule: approval requested

→ request returns — ActionPendingError (2:14)

… Slack card to alice · access-token TTL: 10 min

⏳ the gap — 17 minutes with nothing happening

token expired during the pause (2:31)

alice approves out of band — Slack (2:29)

resumeAction() → single-use capability issued

fresh token minted at execution — not the dead one

mutated args bounce — AuthorizationInputChangedError

executed once · receipt sealed

The lead proof

Naive refresh fails 7/8. nominee gets 8/8. Same agent code.

A mock OAuth server with rotating refresh tokens and real latency. The agent pauses for approval, then fires eight tool calls at once. The natural-but-wrong first attempts (A and D) break; nominee (B and C) does not.

$ node examples/token-refresh-correctness/run.mjs

A) naive — hold the access token across the pause

  resource → 401 token_expired

B) nominee — fresh token at call time

  before → 200 OK | after pause → 200 OK

C) nominee + 8 concurrent calls

  network refreshes = 1 (single-flight) | resource 200s = 8/8

D) refresh WITHOUT single-flight (8 concurrent)

  network refreshes = 8 | invalid_grant failures = 7/8

No API keys · runs offline · the example on GitHub

What breaks after "approve"

The pause is the easy part. The moment after is not.

Once a human approves out of band — Slack, email, push — four things can still go wrong before the tool runs. nominee was built for exactly these four.

Approved out of band · 2:29

refund.issue

Refund $200 for order ord_42

alice approved via Slack · approved once · resumes as a fresh capability

Token expired

The credential minted before the pause died while a human deliberated. nominee resolves tokens at execution, inside the run() callback — a token is never older than the call it serves.

Arguments drifted

The agent reworded the input while you approved. Every approval is bound to a canonical hash of the exact input, so drifted arguments throw AuthorizationInputChangedError instead of executing.

Permission changed

The user lost access while you deliberated. When an action names a resource, the authorizer is consulted while planning and again after the capability is consumed — a permission revoked mid-approval fails closed.

Approval replayed

The approved call runs twice. The capability is single-use and expires; resumeAction() rotates it and invalidates the old value. One approval, one execution.

Add it to your agent

Wrap the tools you already have.

Write the rules, pass your existing tool functions to guard(), and keep the rest of your agent code.

agent.ts
import { Nominee, allow, deny, ask } from 'nominee'

const nominee = new Nominee({
  policy: {
    rules: [
      allow('orders.read'),
      allow('refund.issue', { when: ({ input }) => input.amount <= 50 }),
      ask('refund.issue', { when: ({ input }) => input.amount <= 500 }),
      deny('refund.issue'),
      deny('customers.export'),
    ],
    fallback: 'deny',
  },
})

const tools = nominee.guard({
  'orders.read': readOrder,
  'refund.issue': issueRefund,
  'customers.export': exportCustomers,
}, { user: session.userId })

Approval comes back in Slack, email, or a push — not in this request? The call throws ActionPendingError with a durable action id; persist it and resume later. When the approval outlives the request →

Works with the stack you already run

No rules yet? (a section, not the lead)

Discover what your agent already does.

Two lines wrap your existing tools, report-only: it records which tool callbacks actually start, their argument shapes and numeric ranges, and where nothing bounds the agent's authority. The report below is a sample report from a hard-coded demo agent — run it on your own agent to see yours.

const nominee = new Nominee({ mode: 'observe' })
const tools = nominee.observe(yourTools) // report-only — deny/ask gates stay off
$ npx nominee-cli observe — sample report, demo agent

! ENFORCEMENT WAS OFF: callbacks reached the tools.

refund.issue     5 calls · mutate

  ↳ amount: number, observed 5–2000  [unbounded]

orders.read      3 calls · read

customers.export 1 call  · unknown

Sample output from a demo agent · discovery only, not a security control · read the guide

Supporting proof · prompt injection

The model was hijacked. The tool still did not run.

And the refusal is on the record: sealed into the hash-chained receipt log, tamper-evident against a downstream log editor (anyone with the key can verify — that is not a legal signature). This is blast-radius containment, not detection.

An untrusted email tells the agent to leak the inbox. The model follows the instruction; the denied tool never runs — and the denial is on the record. Same boundary on an MCP server →
  • Denied before the tool runs

    deny throws PolicyDeniedError before your tool function receives control — the hijacked model cannot talk its way past it.

  • §

    Refused on the record

    Refusals are receipted as faithfully as approvals. The half nobody else has: when nominee says no, there is a hash-chained record that it said no, and why.

  • Escalation still goes to a human

    The delete-the-evidence step is escalated to, and denied by, a human — the same out-of-band approval path, sealed into the same chain.

Honest about scope

Does your approval return in the same request? Then you don't need nominee.

The self-selection test: if the human's answer comes back inside the HTTP request that asked for it, the pause never happened, and neither did the staleness. The full list, in the open, below the proof:

  • Your approval comes back in the same request that asked for it — no pause, no staleness.
  • Your agent only reads public or low-risk data.
  • Your framework already enforces every permission your application needs.
  • A few local if-statements cover your tools and you do not need durable approvals or shared limits.

Use nominee when a human approves out of band (Slack / email / push) and the agent takes a write action on a third-party API — and you want that moment after "approve" to be correct, and on the record.

MCP

OAuth authorizes the connection. nominee authorizes the action.

MCP OAuth decides which client may talk to which server. It does not decide whether email.forward may run with these arguments for this user. nominee-mcp wraps the handler so a hijacked model still cannot execute the denied tool.

FAQ

The questions people actually ask.

How do I restrict what my AI agent can do?

Write ordered allow, ask, and deny rules, then wrap your tools with nominee.guard() or a framework adapter. Nominee checks each call before the tool function runs.

How do I require human approval before an agent issues a refund?

Add ask('refund.issue') or another ask rule. The call pauses for your approval UI, and the approval applies only to the arguments the person reviewed.

What happens to the token while an approval is pending?

Nothing — and that's the point. Nominee resolves credentials inside the run() callback, at execution time, after the capability is consumed. A token never goes stale across the pause because it is minted after the pause, not before it.

How do I add permissions to AI tool calls?

Keep your agent framework and tools. Put Nominee around the tools that can change data, spend money, or send messages; its rules decide which calls run.

Isn't this just an if-statement with extra steps?

For one low-risk tool, an if-statement may be enough. Nominee is useful when approvals outlive a request, workers share limits, permissions can change during a pause, or several frameworks need the same rules and receipts.

Can the model talk its way past a deny?

Not if the call routes through Nominee: a deny throws PolicyDeniedError before your tool code runs. Keep raw tools and credentials outside model-controlled code so the wrapper cannot be bypassed.

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. The chain is hash-chained (HMAC): anyone with the key can verify nothing was edited, but anyone with write access to the whole log could also rewrite it consistently. The guarantee is evidence against a downstream log editor, not a legal signature.

How is this different from Arcade, Composio, or Vercel Connect?

Those products can manage connections and tools. nominee is the open-source enforcement layer that binds your application policy to the exact tool action. It can use a managed platform, your IAM, or your own token store underneath.

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?

Does your approval come back in the same HTTP request that asked for it? Then you don't need nominee. Also: a read-only agent with no authority worth guarding, your platform's native permission system covering you end-to-end, or one fully-managed vendor for tools, auth, and policy — use Arcade or Composio directly.

Packages

One authorization layer, wherever your agent runs.

MCP · first class

nominee-mcp

Register guarded tools on the official MCP server SDK. OAuth connects; nominee decides which call runs.

Zero install

nominee-cli

Run the offline refund + replay-proof demo with no clone, build, or API key. Also verify receipts and check policy files.

Core

nominee

Policy engine, approvals, hash-chained receipts, token brokering. Zero runtime deps.

Vercel AI SDK

nominee-ai

guardTools wraps your AI SDK tools in one line; nomineeTool for per-tool config. Runs on Cloudflare Agents too.

Vercel Eve

nominee-eve

Policy enforcement, portable approvals, and fresh tokens inside Eve tools.

Optional · Strategy

nominee-supabase

Store provider tokens in Supabase; nominee reads and refreshes them. Zero deps.

Optional · Managed

nominee-auth0

Auth0 Token Vault for federated tokens, and CIBA approvals on the user's phone.

Production storage

nominee-postgres

Transactional actions, budgets, single-use capabilities, outcomes, journals, and receipt streams.

OpenAI Agents SDK

nominee-openai

Decision-bound tools with Nominee ask rules mapped to native resumable approvals.

Mastra

nominee-mastra

Mastra tools with native or portable durable approval and execution-time credentials.

LangChain JS

nominee-langchain

LangChain structured tools whose side effects run through nominee.run().

Make the moment after "approve" correct.

Run the 7/8 → 8/8 proof, then wrap one of your own tools. Same agent code, no signup, no API key.

Read the docs → ★ Star on GitHub

Or try the live playground →