Approval is the easy part · MIT

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

Pausing for approval is the easy part. Tokens expire, arguments drift, and permissions change while you wait. nominee checks all three the moment the call actually runs, and writes a receipt.

token minted at execution · bound to the args a human saw · spendable once

No signup · no API key · runs offline

the proof: nominee 8/8, naive refresh 1/8
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. Two common implementations (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 change before the tool runs. nominee covers each one.

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 })

If approval comes back in Slack, email, or a push after this request ends, 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?

Discover what your agent already does.

Two lines wrap your tools in report-only mode. nominee records which callbacks start, their argument shapes and ranges, and where the agent's authority is unbounded. The report below comes from a hard-coded demo agent. Run it on your 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.

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. When nominee says no, a hash-chained record shows the refusal and the reason.

  • Escalation still goes to a human

    A human denies the delete-the-evidence step through the same out-of-band approval path. The denial enters the same receipt chain.

Honest about scope

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

If the answer returns in the same HTTP request, state has no time to go stale. You also do not need nominee when:

  • Your approval comes back in the same request that asked for it.
  • 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), the agent takes a write action on a third-party API, and you want the moment after "approve" 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 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?

No token waits with it. 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' } for the full input instead, or 'none' to skip it entirely.

Can receipts be forged?

Tamper-evident, not non-repudiation. 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. 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, then tighten the policy once you know what your agent actually does.

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. A read-only agent has no authority worth guarding. A platform whose native permission system covers you end-to-end already handles this. If one managed vendor covers 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.

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 →