In about five minutes: install toolpass, ask it a question, put it in front of an agent tool, then
run the same thing as a server. You need Python 3.10 or later; step 3 also uses Docker (or
nothing more than the same pip install) and, for the Node client, Node 18.17 or later. Nothing
here touches a real system; step 4 connects one.
python3 -m venv .venv && . .venv/bin/activate
pip install toolpass
curl -sO https://raw.githubusercontent.com/roee-hersh/toolpass/main/examples/toolpass.yamlThe example config has a demo connection that talks to nothing. In it, admin@example.com may
write and dana@example.com may not. The engine runs in your process:
python3 -c '
from toolpass import Toolpass
tp = Toolpass.from_config("toolpass.yaml")
print(tp.check("dana@example.com", "demo", "thing.write", "thing:1"))
'Decision(decision='deny', reason='denied: dana@example.com is not an admin', status=200)
Change the user to admin@example.com and the answer is allow. Try nobody@example.com and it is
deny with user_not_found. Every answer is also a JSON line on stderr: that is the
decision log, set by decision_log in the file.
The command line asks the same question without any code:
toolpass check -config toolpass.yaml -connection demo \
-user dana@example.com -action thing.write -resource thing:1Wrap the function your agent calls, so it runs only after toolpass said allow. The user comes from
your application, never from the model. Save as quickstart.py:
from contextvars import ContextVar
from toolpass import Toolpass, PermissionDenied, guarded
tp = Toolpass.from_config("toolpass.yaml")
current_user: ContextVar[str] = ContextVar("current_user")
@guarded(tp, "demo", "thing.write", "thing:{thing_id}", user=current_user)
def write_thing(thing_id: str) -> str:
return f"wrote thing {thing_id}" # your real action goes here
for who in ["admin@example.com", "dana@example.com"]:
current_user.set(who) # in a real agent: the signed-in user, never the model
try:
print(who, "->", write_thing(thing_id="1"))
except PermissionDenied as e:
print(who, "-> refused:", e.decision.reason)python3 quickstart.py 2>/dev/null # stderr carries the decision logadmin@example.com -> wrote thing 1
dana@example.com -> refused: denied: dana@example.com is not an admin
In a real agent, use your framework's adapter instead, configured once on the agent
(toolpass.strands, toolpass.langchain, toolpass.mcp, ...), or put the framework's tool
decorator on top of guarded. The agent guide covers each framework and
where the user comes from.
In-process, the agent's process holds toolpass's lookup credentials. To keep them out of it, run the same engine as a service and point the agent at it:
docker run --rm -p 8080:8080 -e TOOLPASS_API_KEY=change-me \
-v "$PWD/toolpass.yaml:/etc/toolpass/toolpass.yaml:ro" ghcr.io/roee-hersh/toolpassNo Docker? The package you installed is the server too:
TOOLPASS_API_KEY=change-me toolpass serve -config toolpass.yaml.
In a second terminal:
curl -X POST localhost:8080/check -H 'Authorization: Bearer change-me' \
-d '{"user":"dana@example.com","connection":"demo","action":"thing.write","resource":"thing:1"}'{"decision":"deny","reason":"denied: dana@example.com is not an admin"}In quickstart.py, change one line and run it again; the output is the same:
tp = Toolpass.remote("http://localhost:8080", "change-me") # or TOOLPASS_URL and TOOLPASS_API_KEYNode talks to the server with toolpass-client. Save as quickstart.mjs (the .mjs
extension lets it use import):
npm install toolpass-clientimport { AsyncLocalStorage } from "node:async_hooks";
import { Toolpass, PermissionDenied, guarded } from "toolpass-client";
const tp = new Toolpass(); // reads TOOLPASS_URL and TOOLPASS_API_KEY
const currentUser = new AsyncLocalStorage();
const writeThing = guarded(tp, "demo", "thing.write", "thing:{thingId}", { user: currentUser })(
async ({ thingId }) => `wrote thing ${thingId}`, // your real action goes here
);
for (const who of ["admin@example.com", "dana@example.com"]) {
try {
console.log(who, "->", await currentUser.run(who, () => writeThing({ thingId: "1" })));
} catch (e) {
if (!(e instanceof PermissionDenied)) throw e;
console.log(who, "-> refused:", e.decision.reason);
}
}TOOLPASS_URL=http://localhost:8080 TOOLPASS_API_KEY=change-me node quickstart.mjsPick the system your agent acts in, follow its integration page to create
a read-only credential, and add a connection to toolpass.yaml. For example, Jira:
connections:
- id: jira-main
integration: jira
url: https://acme.atlassian.net
username: toolpass-bot@acme.com
credential: env:JIRA_TOKENThen check the file and the credential before you rely on it:
export JIRA_TOKEN=... # the token from the integration page
export TOOLPASS_API_KEY=change-me # validate reads api_key when the file has one
toolpass validate -config toolpass.yaml
toolpass probe -config toolpass.yaml -connection jira-main
toolpass check -config toolpass.yaml -connection jira-main \
-user you@acme.com -action DELETE_ISSUES -resource issue:PAY-123toolpass catalog jira lists the actions you can ask about. The integrations that sign with a
private key (a GitHub App, Google service accounts, Snowflake key pairs, Salesforce JWT, Microsoft
365 certificates) need pip install "toolpass[crypto]"; the Docker image has it.
- Architecture: how a check flows, what is cached, and the trust boundaries.
- Deploy: in-process or as a server, with Docker, pip or Helm, and how to reach it over TLS.
- Add toolpass to your agent: the adapters for Strands, LangChain and LangGraph, MCP, the OpenAI Agents SDK, the Claude Agent SDK, Google ADK, CrewAI, Pydantic AI, LlamaIndex, and the TypeScript client.