npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nominee-ai

v3.0.0

Published

One-line guardTools for Vercel AI SDK and Cloudflare Agents — user, tenant, resource, connection, scopes flow into policy and fresh tokens.

Readme


Installation

npm i nominee nominee-ai

Also works with Cloudflare Agents — they use the same AI SDK internals.


Observe Before Enforcing

Pass new Nominee({ mode: 'observe' }) to guardTools, nomineeTool, or withNominee to inventory the tool callbacks that actually run before writing a policy. The adapter still routes through run(), but denies, asks, budgets, and approval: true are recorded rather than enforced. Observation reports do not retain raw string/boolean values or user IDs; numeric aggregates may be sensitive. Observe mode is not a security control and cannot be combined with production: true.


How It Works

flowchart LR
    LLM["LLM decides to\ncall a tool"] --> G["guardTools() /\nnomineeTool()"]
    G --> RUN["nominee.run()\ndecision-bound path"]
    RUN --> P{"policy:\nallow / deny / ask"}
    P -->|deny| X["PolicyDeniedError\n(tool never runs)"]
    P -->|ask| AP["⏸ wait for a\nhuman decision"]
    AP -->|pending| APE["ActionPendingError\n(durable action id)"]
    AP -->|approved| CAP["consume capability\n(exact input hash)"]
    P -->|allow| CAP
    CAP --> TOK["strategy resolves\ntoken (optional)"]
    TOK --> EX["execute(input, ctx)"]
    EX --> R["receipt appended\nhash-chained record"]

Every call routes through nominee.run() — the decision-bound path that binds authorization to a fingerprint of the arguments and issues a single-use capability before execute runs. Denied calls throw PolicyDeniedError; ask calls block until a human decides or surface ActionPendingError with a durable action id when the approval outlives the request; every outcome (including refusals) lands on the receipt chain.


Quickstart — guardTools

Wrap your existing AI SDK tools object in one line. The object key is the tool name your policy matches on:

import { Nominee, allow, deny, ask } from 'nominee'
import { guardTools } from 'nominee-ai'
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'

const nominee = new Nominee({
  policy: {
    rules: [
      allow('searchEmail'),
      deny('forwardEmail', { reason: 'external forwarding is exfiltration' }),
      ask('mergePr'), // a human decides, every time
    ],
    fallback: 'deny',
  },
  onApprovalRequest: (req) => notifySlack(req), // req.approve() / req.deny()
})

const { text } = await generateText({
  model: openai('gpt-4o'),
  tools: guardTools(nominee, { searchEmail, forwardEmail, mergePr }, { user: 'alice' }),
  prompt: 'Triage my inbox',
})

Your tools are unchanged — guardTools intercepts each execute, calls nominee.run() with the call's full context, and only then runs the original. Client-executed tools (no execute) pass through untouched.

Full context, still one line

The third argument carries the same per-call context as nomineeTool. user can be an async resolver of the tool-call options: (options) => session.userId. resource and tenant can be static values or resolvers of (input, options), and connection / scopes feed your tokens strategy. Every resolved value reaches nominee.run(), so tenant- and resource-scoped policy rules, external authorization, and token strategies all see it:

const tools = guardTools(
  nominee,
  { searchEmail, forwardEmail, deleteRepo },
  {
    user: 'alice',
    tenant: 'acme', // or (input, options) => session.tenant
    resource: (input) => input.to?.mailbox, // only this mailbox, per call
    connection: 'google', // fresh token for this connection at call time
    scopes: ['gmail.send'],
  },
)

// Policy can then scope on the context the one-liner carries:
// allow('email.forward', { when: ({ tenant }) => tenant === 'acme' })

Note: connection / scopes on guardTools authorize a fresh token through your strategy (policy when clauses, external authorization, and the receipt log all see it), but the wrapped tool's plain AI SDK execute receives only (input, options) — it never sees the token. When the tool itself must call the third-party API, use nomineeTool, whose execute receives the fresh token in ctx.token. And because resolving a token requires a configured strategy, a connection on a policy-only nominee fails closed at call time.

Which path to use when

  • guardTools — wrap your whole existing tools object with one shared context: user, resource, tenant, connection, scopes for every tool. Your tools keep their plain AI SDK execute signature — they do not receive ctx.token; connection / scopes there are for authorization and audit, not for handing the tool a secret.
  • nomineeTool — per-tool config: a different connection / scopes / approval / policy action per tool, and the fresh token injected into ctx.token where your execute actually consumes it.

nomineeTool — Per-Tool Config

When each tool needs its own connection / scopes, a forced approval, its own policy action name, or the fresh token inside ctx.token, build it with nomineeTool:

import { nomineeTool } from 'nominee-ai'
import { z } from 'zod'

const starRepo = nomineeTool({
  nominee,
  user: 'user_123',
  connection: 'github',       // fresh token injected into ctx.token at call time
  action: 'github.star',      // the tool name your policy rules match on
  description: 'Star a GitHub repository',
  inputSchema: z.object({ repo: z.string() }),
  execute: async ({ repo }, ctx) => {
    await fetch(`https://api.github.com/user/starred/${repo}`, {
      method: 'PUT',
      headers: { Authorization: `Bearer ${ctx.token}` },
    })
    return `Starred ${repo}`
  },
})

The policy is enforced here too — nomineeTool routes through nominee.run() on action (default "tool") before execute runs, then resolves the token via your nominee strategy inside the capability callback (fresh at call time, single-flight refresh). Pass resource, tenant, and scopes when your policy or strategy needs them.


Forcing an Approval

approval: true forces an ask even when the policy allows the call — the tool pauses until a human decides, and a denial throws ApprovalDeniedError before execute:

const deleteRepo = nomineeTool({
  nominee,
  user: 'user_123',
  connection: 'github',
  approval: true,               // ⏸ pauses until a human approves
  action: 'repo.delete',
  description: 'Delete a GitHub repository',
  inputSchema: z.object({ repo: z.string() }),
  execute: async ({ repo }, ctx) => {
    // Only runs after explicit human approval
    await fetch(`https://api.github.com/repos/${repo}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${ctx.token}` },
    })
    return `Deleted ${repo}`
  },
})

For rule-driven escalation (ask('repo.delete'), argument-level when conditions, maxCalls budgets), put it in the policy instead — the decision and its resolution are sealed into the receipt chain either way.


What happens on ask

ask rules (and approval: true) route through nominee.run(). If a human settles the approval inline — e.g. your onApprovalRequest calls req.approve() within the same request — the tool runs right away. If the approval outlives the request (the callback only notifies, a CIBA push is still pending, or the process goes away first), the tool's execute throws ActionPendingError with a durable actionId instead of hanging — and the tool never runs. Catch it where the call is made, persist the actionId and the original input (the durable action record stores only an input hash), then resume later with resolveActionApproval()resumeAction()executeCapability(). Full walkthrough: Approvals that outlive the request.


withNominee — Set Defaults Once

Apply a shared nominee instance and user context across all tools in one call:

import { withNominee } from 'nominee-ai'

const nomineeTool = withNominee(nominee, {
  user: 'user_123',      // or an async resolver of the tool-call options
})

// All tools share the same user by default
const starRepo = nomineeTool({
  connection: 'github',
  description: 'Star a repository',
  inputSchema: z.object({ repo: z.string() }),
  execute: async ({ repo }, ctx) => starRepoForUser(repo, ctx.token),
})

Tool Context

The execute function of a nomineeTool receives a rich context object:

execute: async (input, ctx) => {
  ctx.token     // string — fresh token for the configured connection (if any)
  ctx.user      // string — the resolved principal
  ctx.ai        // the raw AI SDK tool context (messages, toolCallId, etc.)
}

TypeScript

Full generics are preserved end-to-end:

const tool = nomineeTool({
  inputSchema: z.object({ repo: z.string() }), // input is typed as { repo: string }
  execute: async ({ repo }, ctx) => {           // return type is inferred
    return { starred: repo, at: new Date() }
  },
})

guardTools preserves the type of the tools object you pass in.


CommonJS + ai@7 (Node version floor)

nominee-ai ships both ESM (dist/index.js) and CJS (dist/index.cjs). The peer range is ai: ^5 || ^6 || ^7. From ai@7 the AI SDK is ESM-only: there is no require export. dist/index.cjs's require('ai') works only because Node ≥ 22.12 added synchronous require() of ESM. A CJS consumer on older Node, or a bundler that resolves exports strictly at build time (webpack, ts-jest in CJS mode), will fail to load the .cjs entry once ai@7 is the resolved peer. ai@5 and ai@6 still ship a real require condition.

Prefer the ESM entry (import from nominee-ai). If you must require(), use Node ≥ 22.12, or stay on ai@5/ai@6. This is a packaging trap, not a policy-behavior change. Full notes: docs/adapter-compatibility.md.