nopeai is a tiny, dependency-free permission engine for AI agents.
It answers one question, fast:
can this agent do this action to this resource?
If you want a small permission layer for tools, MCP servers, invoices, tenant
data, or other agent-facing resources, nopeai gives you a plain-object API
with default deny and deny-overrides-allow behavior.
- Small enough to embed directly into an agent runtime or app
- No database, network calls, DSL, or framework lock-in
- Same mental model in TypeScript and Python
- Default deny, with explicit deny rules always winning
- Easy to audit because the rules are just data
import { createPermissionEngine } from "@lexnede/nopeai";
const engine = createPermissionEngine([
{
role: "agent",
action: "call_tool",
resource_type: "tool",
resource_id: "search",
effect: "allow",
},
{
role: "agent",
action: "call_tool",
resource_type: "tool",
resource_id: "email",
effect: "deny",
},
]);
const agent = { id: "agent_1", roles: ["agent"] };
engine.can(agent, "call_tool", { type: "tool", id: "search" }); // true
engine.can(agent, "call_tool", { type: "tool", id: "email" }); // falseThe Python version works the same way:
from nopeai import createPermissionEngine
engine = createPermissionEngine(
[
{
"role": "agent",
"action": "call_tool",
"resource_type": "tool",
"resource_id": "search",
"effect": "allow",
},
{
"role": "agent",
"action": "call_tool",
"resource_type": "tool",
"resource_id": "email",
"effect": "deny",
},
]
)
agent = {"id": "agent_1", "roles": ["agent"]}
assert engine.can(agent, "call_tool", {"type": "tool", "id": "search"}) is True
assert engine.can(agent, "call_tool", {"type": "tool", "id": "email"}) is FalseRules are plain objects with these fields:
roleactionresource_type- optional
resource_id effectas"allow"or"deny"- optional
condition(agent, action, resource, context)
Evaluation order:
- Find matching rules.
- If any match is
deny, deny. - Else if any match is
allow, allow. - Otherwise deny.
Wildcards:
role=""action=""resource_type=""resource_id="*"
Install the TypeScript package with your preferred JavaScript package manager:
npm install @lexnede/nopeai
pnpm add @lexnede/nopeai
yarn add @lexnede/nopeai
bun add @lexnede/nopeaiInstall the Python package with pip or uv:
pip install nopeai
uv add nopeaiBoth implementations expose the same logical API:
createPermissionEngine(rules)can(agent, action, resource, context?) -> booleanauthorize(agent, action, resource, context?)explain(agent, action, resource, context?) -> decision
import {
PermissionDeniedError,
createPermissionEngine,
examples,
sampleAgents,
sampleResources,
} from "@lexnede/nopeai";
const engine = createPermissionEngine([
{
role: "support",
action: "call_mcp_tool",
resource_type: "mcp_tool",
resource_id: "github.create_issue",
effect: "allow",
},
]);
console.log(
engine.can(
sampleAgents.support,
"call_mcp_tool",
sampleResources.createIssueTool
)
); // true
console.log(
examples.tools.can(sampleAgents.agent, "call_tool", sampleResources.searchTool)
); // true
try {
engine.authorize(
sampleAgents.support,
"call_mcp_tool",
sampleResources.deleteRepoTool
);
} catch (error) {
if (error instanceof PermissionDeniedError) {
console.log(error.decision.reason);
}
}from nopeai import PermissionDeniedError, createPermissionEngine
from nopeai.examples import examples, sample_agents, sample_resources
engine = createPermissionEngine(
[
{
"role": "finance",
"action": "read",
"resource_type": "invoice",
"effect": "allow",
"condition": lambda agent, action, resource, context: agent["metadata"].get(
"tenant_id"
)
== resource["metadata"].get("tenant_id"),
}
]
)
assert engine.can(
sample_agents["finance"],
"read",
sample_resources["invoice"],
) is True
assert examples["mcp"].can(
sample_agents["support"],
"call_mcp_tool",
sample_resources["delete_repo_tool"],
) is False
try:
engine.authorize(
sample_agents["agent"],
"call_tool",
sample_resources["email_tool"],
)
except PermissionDeniedError as error:
print(error.decision["reason"])The package ships example engines, rule sets, agents, and resources so you can prototype quickly.
TypeScript:
examplesexampleRulessampleAgentssampleResources
Python:
nopeai.examples.examplesnopeai.examples.example_rulesnopeai.examples.sample_agentsnopeai.examples.sample_resources
Yes. nopeai is framework-agnostic, so it works with LangChain, LangGraph,
and PydanticAI. The integration point is the same in each stack: call
authorize(...) right before executing a tool.
import { z } from "zod";
import { tool } from "@langchain/core/tools";
import { createPermissionEngine } from "nopeai";
const permissions = createPermissionEngine([
{
role: "agent",
action: "call_tool",
resource_type: "tool",
resource_id: "web_search",
effect: "allow",
},
{
role: "agent",
action: "call_tool",
resource_type: "tool",
resource_id: "send_email",
effect: "deny",
},
]);
const identity = { id: "assistant_1", roles: ["agent"] };
export const webSearch = tool(
async ({ query }) => {
permissions.authorize(identity, "call_tool", { type: "tool", id: "web_search" });
return `Search results for: ${query}`;
},
{
name: "web_search",
description: "Search the web for recent information",
schema: z.object({ query: z.string() }),
}
);In LangGraph, the cleanest pattern is a dedicated tool-dispatch node that applies authorization for every selected tool.
import { createPermissionEngine } from "nopeai";
const permissions = createPermissionEngine([
{ role: "support", action: "call_tool", resource_type: "tool", effect: "allow" },
{
role: "support",
action: "call_tool",
resource_type: "tool",
resource_id: "delete_account",
effect: "deny",
},
]);
type State = {
agent: { id: string; roles: string[] };
nextToolId: string;
input: Record<string, unknown>;
};
export function toolNode(state: State) {
permissions.authorize(state.agent, "call_tool", {
type: "tool",
id: state.nextToolId,
});
// execute selected tool here only after authorize(...) passes
return { ...state };
}from nopeai import PermissionDeniedError, createPermissionEngine
from pydantic_ai import Agent
permissions = createPermissionEngine(
[
{
"role": "assistant",
"action": "call_tool",
"resource_type": "tool",
"resource_id": "weather_api",
"effect": "allow",
},
{
"role": "assistant",
"action": "call_tool",
"resource_type": "tool",
"resource_id": "payments_api",
"effect": "deny",
},
]
)
identity = {"id": "assistant_1", "roles": ["assistant"]}
assistant = Agent("openai:gpt-4o-mini")
def authorize_tool(tool_id: str) -> None:
permissions.authorize(identity, "call_tool", {"type": "tool", "id": tool_id})
@assistant.tool
def weather(city: str) -> str:
authorize_tool("weather_api")
return f"Weather for {city}: sunny"
@assistant.tool
def charge_card(amount: float) -> str:
authorize_tool("payments_api")
return f"Charged {amount}"
try:
authorize_tool("payments_api")
except PermissionDeniedError as error:
print(error.decision["reason"]) # deny_rule_matchedIf you are working from this repository instead of the published packages:
npm install
python3 -m pip install -r python/requirements-dev.txtRun tests:
npm testBuild publishable artifacts:
npm run build:typescript
npm run build:pythonnopeai is published to:
npmas@lexnede/nopeaifromtypescript/package.jsonPyPIfrompython/pyproject.toml
Releases use one shared version number across both packages.
- Update both package versions together.
- Push a Git tag in the format
vX.Y.Z. - GitHub Actions validates the tag, runs tests, builds both packages, smoke tests the artifacts, and publishes them.
- Trusted publishing must be configured once in npm and PyPI.