Capability tokens for AI agents - Python SDK
Project description
Tenuo Python SDK
Capability tokens for AI agents
v0.1.0-alpha.9 — See CHANGELOG for breaking changes.
Python bindings for Tenuo, providing cryptographically-enforced capability attenuation for AI agent workflows.
Installation
pip install tenuo
AgentDojo Benchmark: 100% Attack Block Rate
Tested against GPT-5.1 prompt injection attacks—240 successful injections, 0 escaped. Details →
Quick Start
The Safe Path (Recommended)
Keep keys separate from warrants:
from tenuo import Warrant, SigningKey, Pattern
# Warrant in state/storage - serializable, no secrets
warrant = receive_warrant_from_orchestrator()
# Explicit key at call site - keys never in state
key = SigningKey.from_env("MY_SERVICE_KEY")
headers = warrant.auth_headers(key, "search", {"query": "test"})
# Delegation with attenuation
child = warrant.delegate(
to=worker_pubkey,
allow={"search": {"query": Pattern("safe*")}},
ttl=300,
key=key
)
BoundWarrant (For Repeated Operations)
When you need to make many calls with the same warrant+key:
from tenuo import Warrant, SigningKey
warrant = receive_warrant()
key = SigningKey.from_env("MY_KEY")
# Bind key for repeated use
bound = warrant.bind_key(key)
for item in items:
headers = bound.auth_headers("process", {"item": item})
# ...
# Authorize directly
if bound.authorize("search", {"query": "test"}):
# Authorized!
pass
# ⚠️ BoundWarrant is non-serializable (contains key)
# Use bound.warrant to get the plain Warrant for storage
Low-Level API (Full Control)
from tenuo import SigningKey, Warrant, Pattern, Range
# Generate keypair
keypair = SigningKey.generate()
# Issue warrant with fluent builder
warrant = (Warrant.builder()
.capability("manage_infrastructure", {
"cluster": Pattern("staging-*"),
"replicas": Range.max_value(15)
})
.holder(keypair.public_key)
.ttl(3600)
.issue(keypair))
# Authorize with Proof-of-Possession
args = {"cluster": "staging-web", "replicas": 5}
pop_sig = warrant.create_pop_signature(keypair, "manage_infrastructure", args)
authorized = warrant.authorize(
tool="manage_infrastructure",
args=args,
signature=bytes(pop_sig)
)
Installation Options
pip install tenuo # Core only
pip install tenuo[fastapi] # + FastAPI integration
pip install tenuo[langchain] # + LangChain
pip install tenuo[langgraph] # + LangGraph (includes LangChain)
pip install tenuo[mcp] # + MCP client (Python ≥3.10)
pip install tenuo[dev] # Development tools
Key Management
Loading Keys
from tenuo import SigningKey
# From environment variable (auto-detects base64/hex)
key = SigningKey.from_env("TENUO_ROOT_KEY")
# From file (auto-detects format)
key = SigningKey.from_file("/run/secrets/tenuo-key")
# Generate new
key = SigningKey.generate()
KeyRegistry (For LangGraph)
Thread-safe key management for multi-agent workflows:
from tenuo import KeyRegistry, SigningKey
registry = KeyRegistry.get_instance()
registry.register("worker", SigningKey.from_env("WORKER_KEY"))
registry.register("orchestrator", SigningKey.from_env("ORCH_KEY"))
# Retrieve
key = registry.get("worker")
Keyring (For Key Rotation)
from tenuo import Keyring, SigningKey
keyring = Keyring(
root=SigningKey.from_env("CURRENT_KEY"),
previous=[SigningKey.from_env("OLD_KEY")]
)
# All public keys for verification
all_pubkeys = keyring.all_public_keys
FastAPI Integration
from fastapi import FastAPI, Depends
from tenuo.fastapi import TenuoGuard, SecurityContext, configure_tenuo
app = FastAPI()
configure_tenuo(app, trusted_issuers=[issuer_pubkey])
@app.get("/search")
async def search(
query: str,
ctx: SecurityContext = Depends(TenuoGuard("search"))
):
# ctx.warrant is verified
# ctx.args contains extracted arguments
return {"results": [...]}
LangChain Integration
from tenuo import Warrant, SigningKey
from tenuo.langchain import protect
# Create bound warrant
keypair = SigningKey.generate()
warrant = Warrant.builder().tool("search").issue(keypair)
bound = warrant.bind_key(keypair)
# Protect tools
from langchain_community.tools import DuckDuckGoSearchRun
protected_tools = protect([DuckDuckGoSearchRun()], bound_warrant=bound)
# Use in agent
agent = create_openai_tools_agent(llm, protected_tools, prompt)
Using @lockdown Decorator
from tenuo import lockdown, set_warrant_context, set_signing_key_context
@lockdown(tool="read_file")
def read_file(path: str) -> str:
return open(path).read()
with set_warrant_context(warrant), set_signing_key_context(keypair):
content = read_file("/tmp/test.txt") # ✅ Authorized
content = read_file("/etc/passwd") # ❌ Blocked
LangGraph Integration
from tenuo import KeyRegistry
from tenuo.langgraph import secure, TenuoToolNode, auto_load_keys
# Load keys from TENUO_KEY_* environment variables
auto_load_keys()
# Wrap pure nodes
def my_agent(state):
return {"messages": [...]}
graph.add_node("agent", secure(my_agent, key_id="worker"))
graph.add_node("tools", TenuoToolNode([search, calculator]))
# Run with warrant in state
state = {"warrant": warrant, "messages": [...]}
config = {"configurable": {"tenuo_key_id": "worker"}}
result = graph.invoke(state, config=config)
Nodes That Need Warrant Access
from tenuo.langgraph import tenuo_node
from tenuo import BoundWarrant
@tenuo_node
def smart_router(state, bound_warrant: BoundWarrant):
if bound_warrant.preview_can("search"):
return {"next": "researcher"}
return {"next": "fallback"}
Debugging
why_denied() - Understand Failures
result = warrant.why_denied("read_file", {"path": "/etc/passwd"})
if result.denied:
print(f"Code: {result.deny_code}")
print(f"Field: {result.field}")
print(f"Suggestion: {result.suggestion}")
diagnose() - Inspect Warrants
from tenuo import diagnose
diagnose(warrant)
# Prints: ID, TTL, constraints, tools, etc.
Convenience Properties
# Time remaining
warrant.ttl_remaining # timedelta
warrant.ttl # alias for ttl_remaining
# Status
warrant.is_expired # bool
warrant.is_terminal # bool (can't delegate further)
# Human-readable
warrant.capabilities # dict of tool -> constraints
MCP Integration
(Requires Python ≥3.10)
from tenuo.mcp import SecureMCPClient
async with SecureMCPClient("python", ["mcp_server.py"]) as client:
tools = await client.get_protected_tools()
async with root_task(Capability("read_file", path=Pattern("/data/*"))):
result = await tools["read_file"](path="/data/file.txt")
Security Considerations
BoundWarrant Serialization
BoundWarrant contains a private key and cannot be serialized:
bound = warrant.bind_key(key)
# ❌ This raises TypeError
pickle.dumps(bound)
json.dumps(bound)
# ✅ Extract warrant for storage
state["warrant"] = bound.warrant # Plain Warrant is serializable
preview_can() is Not Authorization
# ✅ OK for UI hints
if bound.preview_can("delete"):
show_delete_button()
# ❌ WRONG: Not a security check!
if bound.preview_can("delete"):
delete_database() # No PoP verification!
# ✅ Correct: Use authorize()
if bound.authorize("delete", {"target": "users"}):
delete_database()
Error Details Not Exposed
Authorization errors are opaque by default:
# Client sees: "Authorization denied (ref: abc123)"
# Logs show: "[abc123] Constraint failed: path=/etc/passwd, expected=Pattern(/data/*)"
Examples
# Basic usage
python examples/basic_usage.py
# FastAPI integration
python examples/fastapi_integration.py
# LangGraph protected
python examples/langgraph_protected.py
# MCP integration
python examples/mcp_integration.py
Documentation
- Quickstart — Get running in 5 minutes
- FastAPI — Zero-boilerplate API protection
- LangChain — Tool protection
- LangGraph — Multi-agent security
- Security — Threat model, best practices
- API Reference — Full SDK docs
License
MIT OR Apache-2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tenuo-0.1.0a9.tar.gz.
File metadata
- Download URL: tenuo-0.1.0a9.tar.gz
- Upload date:
- Size: 517.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
907e80f518530ba7f7a0a792ae2f1a53b7aaaeb8438a11ebc121025917d138a9
|
|
| MD5 |
7bea7b782540737536d83c26cadfcc0d
|
|
| BLAKE2b-256 |
02fd0d6fddcf598e663af8c757fc02bfbe8293476a3609ad46f78f90d4a5ac85
|
File details
Details for the file tenuo-0.1.0a9-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: tenuo-0.1.0a9-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 2.1 MB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd5fb0fd691ba00f8d360b9983d2446572f59ce294c7d152ca873b084bb87ca2
|
|
| MD5 |
f72c32e3802fd29e14f524f36d1c8a04
|
|
| BLAKE2b-256 |
6b008ab54aacb2360c2b5d27d032ccf9ca4353ed5f7ef4edd1b91f7fa77bcd5e
|
File details
Details for the file tenuo-0.1.0a9-cp38-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: tenuo-0.1.0a9-cp38-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 2.5 MB
- Tags: CPython 3.8+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29f2318d8a754eabb119457ed46ad2367535c95aa849e9fa0c5e7212df76d243
|
|
| MD5 |
4e0a69c44093ec60c492b6c6d9dd76fe
|
|
| BLAKE2b-256 |
8b4e4c1f3c0df5fc092d0ee8861d7d7d9426942f824c4c47617406f48ad6402b
|
File details
Details for the file tenuo-0.1.0a9-cp38-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: tenuo-0.1.0a9-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.2 MB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d9c27e64efd8bb3a6bb8c64b68040df5de08cae23714a8a06f310e83899b33d
|
|
| MD5 |
f9d545a57a888fd2978cee485df69745
|
|
| BLAKE2b-256 |
e3070e1e111fcfa787ad5f3475881a3729a5f18d95bae61b8ab435e4bcbc7787
|