Documentation

The authorization layer for AI agents. Your agent logs in as you — nominee decides what it can do as you. Bind application authorization, exact-call capability, credential delivery, and durable evidence — dependency-free core, framework-neutral, self-hostable.

What nominee is

nominee sits between the model and your tools. The primary run() path checks application resource authorization and declarative allow / deny / ask policy, reserves budgets, pauses when needed, consumes one exact-input capability, resolves a credential, runs the side effect once, and records its outcome.

The core is dependency-free. Think of it as the Passport.js of agent authorization — scoped to the multi-framework, no-lock-in, standalone tail, where you want this behaviour without buying into one vendor's runtime.

When you don't need nominee

nominee is a focused utility, not a platform. Skip it when something you already have does the job:

  • A read-only agent with no authority worth guarding. Nothing to deny; nothing to receipt.
  • 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 need application authorization, constrained credential delivery, and evidence that are framework-neutral and self-hostable — the same enforcement contract wherever your agent runs.

Framework integrations

Read the framework-specific integration guides in the repository:

Quickstart

npm i nominee

The whole setup is a policy and one line around your tools. No provider, no signup.

import { Nominee, allow, deny, ask } from 'nominee'

const nominee = new Nominee({
  policy: {
    rules: [
      allow('email.read'),
      allow('email.forward', { when: ({ input }) => input.to.endsWith('@acme.com') }),
      deny('email.forward', { reason: 'external forwarding is exfiltration' }),
      ask('email.delete'),
    ],
    fallback: 'deny',
  },
  onApprovalRequest: (req) => notifySlack(req), // req.approve() / req.deny()
})

// One line. Works with plain functions or any framework's { execute } tools.
const tools = nominee.guard({ 'email.read': readEmail, 'email.forward': forwardEmail }, {
  user: 'alice',
})

Denied calls throw PolicyDeniedError before the tool runs. An ask can resolve inline or surface ActionPendingError with a durable action id for later resume. Every outcome — allow, deny, ask, and the eventual approval — lands on the receipt chain.

When tools need third-party tokens

Tools that act on APIs also need credentials — fresh ones, at call time, never sitting in the model's context. Add a strategy; it's optional, and a plain function is the simplest one:

const nominee = new Nominee({
  policy, // as above
  strategy: ({ connection }) => process.env[`${connection.toUpperCase()}_TOKEN`]!,
})

// Decision-bound: credential resolved inside execute after capability consumption
await nominee.run(
  { tool: 'github.issue.close', input: { repo, issue }, user: 'alice', connection: 'github' },
  ({ token }) => closeIssue({ repo, issue, token }),
)

Rule of thumb: never store the returned token in your own agent state. Under run(), the strategy resolves credentials inside the execute callback — see Token freshness for why. Standalone nominee.token() remains for dev; it is disabled under production: true.

Don't want to bring your own tokens? Let Auth0 manage Token Vault + push approvals, also one line: new Nominee({ strategy: auth0() }) — see Strategies.

Policy

Declarative allow / deny / ask rules over tool calls — the model cannot talk its way past a deny. Semantics are small enough to hold in your head:

  • First match wins within a policy; rules are checked in order.
  • No match → fallback (default 'ask' — unknown actions reach a human; set 'deny' for default-deny, or 'allow' for report-only auditing).
  • when predicates see { tool, input, user, tenant, resource, chain } — gate on trusted application context and arguments, not just tool names.
  • Glob patterns match tool names: allow('github.*'), deny('*.delete').
  • Budgets: allow('search.*', { max: 20 }) — the 21st call escalates to a human instead of failing silently.
import { allow, deny, ask } from 'nominee'

allow('email.forward', { when: ({ input }) => input.to.endsWith('@acme.com') })
deny('email.forward', { reason: 'external forwarding is exfiltration' })
ask('repo.delete', { reason: 'destructive', timeoutMs: 5 * 60_000 })
allow('search.web', { max: 20 })  // budget: call #21 asks a human

Dry-run any call without consuming budgets, asking anyone, or writing a receipt:

await nominee.check({ tool: 'repo.delete', user: 'alice' }) // → { effect: 'deny', ... }

Approvals

The legacy single-process approve() API blocks until a decision and throws ApprovalDeniedError if denied or expired.

try {
  await nominee.approve({
    user: 'alice',
    action: 'repo.delete',
    detail: { repo: 'alice/old-project' },
    timeoutMs: 5 * 60_000, // optional; default waits indefinitely
  })
  // approved — proceed
} catch (err) {
  // ApprovalDeniedError: denied or expired
}

If the strategy implements native approval (e.g. Auth0 CIBA), nominee delegates to it. Otherwise the built-in engine pauses until your webhook resolves it:

// in your Nominee config:
onApprovalRequest: (req) => notifyUser(req), // req.id, req.user, req.action, req.detail

// later, from your approval webhook/handler:
nominee.resolveApproval(req.id, 'approved') // or 'denied'

For a durable workflow, use prepareAction(). Persist the pending action id, settle it with resolveActionApproval() (or poll a provider with resumeAction()), then execute the returned one-use capability. run() and framework adapters throw ActionPendingError when that approval outlives the current request.

See a deployed pause/resume in action: nominee.dev/agent — a Cloudflare Durable Object agent that hibernates while it waits for your approval (email link or Auth0 Guardian push), then resumes the same hash-chained receipt log and fetches a fresh GitHub token only at that moment.

Receipts

Every decision, approval, and token grant appends to a hash chain — each receipt's hash covers its content plus the previous hash, so editing or deleting any record breaks verification of everything after it. With a signing key, hashes become HMACs, so only key-holders can forge the chain.

const nominee = new Nominee({
  policy,
  receipts: {
    key: process.env.RECEIPT_KEY,          // optional HMAC signing
    onReceipt: (r) => auditLog.write(r),   // stream to your sink
  },
})

nominee.receipts          // the chain so far
nominee.verifyReceipts()  // { ok: true, checked: 128 }

// Later, offline, from your log sink:
import { verifyReceipts } from 'nominee'
verifyReceipts(exported, { key })  // { ok: false, brokenAt: 41, reason: '…' }

By default inputs are recorded as inputHash (SHA-256 of the canonical JSON) — provable without writing user data into logs. receipts: { input: 'raw' } or 'none' change that. Pass receipts: false to disable receipts entirely.

For multi-replica production, use the atomic receipt store from nominee-postgres. It sequences one stream transactionally:

const control = new PostgresControlStore(postgresDatabase(pool))
const nominee = new Nominee({
  production: true,
  policy,
  actionStore: control,
  receipts: {
    store: control,
    stream: `tenant:${tenantId}`,
    key,
    delivery: 'strict',
  },
})

The action store also persists budgets, approvals, capabilities, outcomes, and an immutable transition journal. In-memory stores remain available for local work. Anchor signed stream tips outside the primary database when the threat model includes rollback of both receipts and their internal checkpoint.

Delegation

An orchestrator can spawn sub-agents whose policies can only narrow authority, never widen it. Across a delegation chain, the strictest outcome wins (deny > ask > allow) — a sub-agent can never allow what its parent denies.

const researcher = nominee.delegate('researcher', {
  policy: [deny('email.*'), deny('github.merge_*')],
})
// researcher's receipts carry chain: ['orchestrator', 'researcher']

The sub-agent shares the parent's token cache, receipt chain, and audit stream, but every event from it carries the extended identity chain — so a delegated action is attributable to the exact sub-agent that took it. A sub-agent's own fallback defaults to 'allow' (its rules are restrictions layered on top of the chain, not a fresh default-ask).

Audit

Every privileged op emits an event to onAudit / on(). Event type is one of:

policy.decision · token.issued · token.cached · token.error · token.invalidated · token.exchanged · approval.requested · approval.resolved · authz.checked

Each event includes { type, user, agent?, connection?, action?, resource?, decision?, effect?, rule?, reason?, chain?, at, detail? }. effect, rule, and reason are set on policy.decision events — the same fields recorded on the matching receipt. chain is the delegation chain (e.g. ['orchestrator', 'research-agent']), at is epoch ms. Pipe it to durable storage so you can always answer "who authorized this, and on whose behalf?".

Token freshness done right

"Just refresh the token" is a five-line happy path until you meet two things real providers do: short-lived access tokens and rotating refresh tokens (GitHub Apps, Google one-time-use, Okta, Auth0 rotation). Then the naive approaches break:

  • Grab-up-front breaks across a pause. An agent that captures the access token, then waits for human approval, acts with an expired token → 401.
  • Naive refresh breaks under concurrency. A real agent fires many tool calls at once. Each reads the same stored refresh token and refreshes independently; rotation invalidates the others mid-flight → invalid_grant, and your stored token can corrupt.

nominee makes both correct, with no change to your tool code:

  • Fresh at call time. Cached per (user, connection) until expiryLeewayMs before expiry (default 60s), then refreshed.
  • Single-flight. Concurrent cache-misses for the same key share one network refresh — no stampede, no rotation race.
  • Rotation persisted. When the provider rotates the refresh token, nominee hands it back via onRefreshToken so the next refresh uses the live token.
See the runnable proof — naive concurrent + rotating refresh fails 7/8, nominee gets 8/8 with the same agent code: examples/token-refresh-correctness.

With the OAuth2 strategy, the one line that makes rotation correct is onRefreshToken:

import { Nominee, OAuth2 } from 'nominee'

const nominee = new Nominee({
  strategy: OAuth2({
    connections: {
      github: {
        tokenEndpoint: 'https://github.com/login/oauth/access_token',
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET,
        refreshToken: () => store.get('alice').refreshToken,   // read from your store
        onRefreshToken: (_p, rt) =>                            // write the rotated one back
          store.set('alice', { ...store.get('alice'), refreshToken: rt }),
      },
    },
  }),
})

Need to drop a token immediately (e.g. you revoked access upstream)? Call nominee.invalidate(user, connection); the next token() re-resolves. token({ force: true }) bypasses the cache for one call.

Strategies

A strategy tells nominee how to get a token — it's the seam that lets you swap vaults without rewriting your agent. Pass one to new Nominee({ strategy }). A plain function is the simplest.

StrategyUse it for
(params) => tokenThe default. A function returning a string or { token, expiresAt?, scopes? }.
tokens(fn)The same, but named — wrap an env var or DB lookup.
OAuth2({ connections })Generic refresh-token flow. Zero deps. Bring your stored refresh tokens; set onRefreshToken for rotating providers.
Memory({ tokens })Seed tokens in memory for dev, examples, and tests.
Supabase({ url, key })Read & refresh provider tokens stored in Supabase. From nominee-supabase.
auth0() / Auth0({ … })Optional managed strategy — Token Vault + CIBA. From nominee-auth0.

Implementing your own is one method, getToken() — see CONTRIBUTING.md.

Auth0 (optional managed upgrade)

Same API — swap the strategy to get managed Token Vault tokens and CIBA phone approvals, with no change to your agent code. For a single tenant, auth0() reads everything from the environment:

import { Nominee } from 'nominee'
import { auth0 } from 'nominee-auth0'

// reads AUTH0_DOMAIN / CLIENT_ID / CLIENT_SECRET / REFRESH_TOKEN / USER_SUB
const nominee = new Nominee({ strategy: auth0() })   // managed Token Vault + CIBA

const token = await nominee.token({ user, connection: 'github' }) // always fresh

Unconfigured, auth0() falls back to a built-in mock (short-TTL token + auto-approve) so examples run with zero setup — set the env and the same call becomes real. For per-request subjectToken, multi-tenancy, or custom CIBA options, use the explicit Auth0({ … }) form.

Framework adapters

Drop nominee into any tool: every official adapter routes the side effect through the decision-bound action lifecycle and resolves credentials only after capability consumption.

import { nomineeTool } from 'nominee-ai' // or 'nominee-eve'
import { z } from 'zod'

const starRepo = nomineeTool({
  nominee,
  user: 'alice',          // string, or a function of the call context
  connection: 'github',   // inject a fresh token into execute()
  approval: true,         // pause for human approval first (optional)
  action: 'github.star',  // name used in the approval prompt + audit
  description: 'Star a GitHub repository',
  inputSchema: z.object({ repo: z.string() }),
  async execute({ repo }, { token, user }) {
    await fetch(`https://api.github.com/user/starred/${repo}`, {
      method: 'PUT',
      headers: { Authorization: `Bearer ${token}` },
    })
    return `Starred ${repo}`
  },
})

nominee-ai targets the Vercel AI SDK (and Cloudflare Agents, which use it). nominee-eve targets Vercel Eve and returns a real defineTool(). The execute context is { token?, user, ai } for AI SDK and { token?, user, eve } for Eve. The ai-sdk-minimal example is the whole integration in ~25 lines.

RuntimePackage
Vercel AI SDK / Cloudflare Agentsnominee-ai
Vercel Evenominee-eve
OpenAI Agents SDKnominee-openai — ask rules use native resumable approval
Mastranominee-mastra — native or portable approval
MCP server SDKnominee-mcp — registered decision-bound handlers

Production

Set production: true on consequential paths. Nominee refuses to start without a default-deny policy, durable action state, an atomic durable receipt store, strict receipt delivery, and durable state for provider-native approvals.

const control = new PostgresControlStore(postgresDatabase(pool))
const nominee = new Nominee({
  production: true,
  policy: { rules, fallback: 'deny' },
  policyVersion: '2026-07-29.1',
  actionStore: control,
  receipts: { store: control, stream: `tenant:${tenant}`, key, delivery: 'strict' },
  authorizer: ({ user, action, resource, tenant }) =>
    appAuthz.can({ user, action, resource, tenant }),
  strategy: credentialBroker,
})

Use the Nominee action id as the downstream API idempotency key. Keep raw tools and root credentials outside model-controlled code. See the repository production runbook for recovery, monitoring, and known boundaries.

Core API

MethodReturns / behavior
run({ tool, input, user, resource?, tenant?, connection?, scopes? }, execute)Recommended decision-bound path: authorize, issue and consume a single-use capability, resolve the credential, execute, and record the outcome.
prepareAction() · resumeAction() · executeCapability()Durable pause/resume API for jobs, approvals, and cross-process execution.
resolveActionApproval(actionId, resolution)Resolve a durable dashboard or webhook approval with approver identity and mechanism.
authorize({ tool, input?, user })Promise<Authorization> — checks the call against the policy. Resolves when it may proceed; throws PolicyDeniedError / ApprovalDeniedError otherwise. requireApproval: true forces an ask.
await assertUnchanged(authorization, input)Verifies that the input about to execute matches the canonical fingerprint evaluated by policy and any approver. Official wrappers call it automatically.
check({ tool, input?, user })Promise<PolicyDecision> — dry run: the decision a call would get, with no side effects (no budgets, no approvals, no receipts).
guard(tools, { user })Wraps a whole tools object (plain async functions or { execute } tools) so every call is authorized first — the one-line integration.
receipts · verifyReceipts() · flushReceipts()The hash-chained, optionally HMAC-signed receipt log, tamper check, and async sink checkpoint. delivery: 'strict' waits for the sink before async authorization returns.
token({ user, connection, scopes?, force? })Promise<string> — a fresh token. Cached per canonical (user, connection, scope set) until just before expiry; force: true bypasses the cache.
approve({ user, action, detail?, timeoutMs? })Promise<ApprovalResult> — resolves on approval; throws ApprovalDeniedError on deny/expiry.
resolveApproval(id, 'approved' | 'denied')boolean — settle a built-in-engine approval from your webhook. Returns false for unknown ids.
can({ user, action, resource })Promise<boolean> — fine-grained authz. Requires a strategy that implements it (else throws).
invalidate(user, connection)boolean — drop the cached token so the next call re-resolves. Returns true if an entry was removed.
delegate(actor, { policy? })Nominee — a sub-agent sharing this cache, receipts & audit stream. Extra rules can only narrow authority (deny > ask > allow); its events carry user → … → actor.
exchange({ user, connection, actor, scopes? })Promise<string> — RFC 8693 downscoped token bound to a sub-agent. Requires a strategy that implements it.
on((event) => ...)() => void — subscribe to audit events; returns an unsubscribe function.

Constructor options also include actionStore?, authorizer?, policyVersion?, production?, and onGovernedAction?. Use usageReporter() for opt-in pseudonymous governed-principal measurement.

Errors

  • PolicyDeniedError — thrown by authorize() and guard()-wrapped tools when a deny rule (or the fallback) matches — before the tool runs. Carries the decision and the matched rule.
  • ActionPendingError — a decision-bound action is durably waiting for approval. Carries .actionId and .approvalId.
  • ApprovalDeniedError — thrown by approve() or run() when a request is denied or expires. Carries .result.
  • AuthorizationInputChangedError — thrown when the arguments about to execute differ from those authorized.
  • ActionOutcomePersistenceError — a governed action reached or needed to record a terminal result, but its durable outcome or evidence failed to persist. Inspect the action and downstream idempotency state before retrying.
  • Strategy errors propagate from token() — e.g. a refresh failure or an unknown connection. A token.error audit event is emitted first.
  • can() throws if the active strategy doesn't implement authorization.

Security model

  • Use the decision-bound path. run() or prepare/resume/execute binds the exact call through outcome; legacy authorize() and unbound token() are disabled in production mode.
  • Use durable stores. nominee-postgres makes budgets and capability consumption atomic and sequences receipts across replicas.
  • Keep root authority outside the runtime. Only the execution callback receives the resolved credential after capability consumption.
  • Make downstream writes idempotent. Use the action id as the third-party API idempotency key.
  • Attribute delegated actions. Use delegate() / exchange() so a sub-agent's actions carry the full identity chain.
  • Report vulnerabilities privately — see SECURITY.md.

Migrating to 2.2

Existing non-production uses of authorize(), token(), approve(), and onAudit remain available. The 2.2 adapters now execute through the decision-bound action lifecycle, so upgrade nominee and its adapters together.

What changed:

  • run() and prepare/resume/execute bind policy, resource authorization, approval, capability, credential, exact input, and outcome.
  • Long-lived adapter approvals may now throw ActionPendingError; persist the action id and resume it instead of holding a process open.
  • production: true disables unbound authorize() / token() and requires default-deny policy plus durable action, receipt, and provider-approval state.
  • nominee-postgres, nominee-openai, nominee-mastra, and nominee-mcp are new first-class packages.