Skip to main content

Capability tokens for AI agents - Python SDK

Project description

Tenuo Python SDK

Capability tokens for AI agents

PyPI Python Versions

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

Open In Colab Explorer

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 from orchestrator - Warrant() accepts base64 string
warrant = Warrant(received_warrant_string)

# Explicit key at call site - keys never in state
key = SigningKey.from_env("MY_SERVICE_KEY")
headers = warrant.headers(key, "search", {"query": "test"})

# Delegation with attenuation
child = warrant.grant(
    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 = Warrant(received_string)  # Reconstruct from base64
key = SigningKey.from_env("MY_KEY")

# Bind key for repeated use
bound = warrant.bind(key)

for item in items:
    headers = bound.headers("process", {"item": item})
    # ...

# Validate before use
result = bound.validate("search", {"query": "test"})
if result:
    # Authorized!
    pass

# ⚠️ BoundWarrant is non-serializable (contains key)
# Use bound.warrant to get the plain Warrant for storage

Low-Level API (Full Control)

# ┌─────────────────────────────────────────────────────────────────┐
# │  CONTROL PLANE / ORCHESTRATOR                                   │
# │  Issues warrants to agents. Only needs agent's PUBLIC key.      │
# └─────────────────────────────────────────────────────────────────┘
from tenuo import SigningKey, Warrant, Pattern, Range, PublicKey

issuer_key = SigningKey.from_env("ISSUER_KEY")
agent_pubkey = PublicKey.from_env("AGENT_PUBKEY")  # From registration

warrant = (Warrant.mint_builder()
    .capability("manage_infrastructure", {
        "cluster": Pattern("staging-*"),
        "replicas": Range.max_value(15)
    })
    .holder(agent_pubkey)
    .ttl(3600)
    .mint(issuer_key))

# Send warrant to agent: send_to_agent(str(warrant))
# ┌─────────────────────────────────────────────────────────────────┐
# │  AGENT / WORKER                                                 │
# │  Receives warrant, uses own private key for Proof-of-Possession │
# └─────────────────────────────────────────────────────────────────┘
from tenuo import SigningKey, Warrant

agent_key = SigningKey.from_env("AGENT_KEY")  # Agent's private key (never shared)
warrant = Warrant(received_warrant_string)    # Deserialize from orchestrator

args = {"cluster": "staging-web", "replicas": 5}
pop_sig = warrant.sign(agent_key, "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()

Key Management

KeyRegistry (Thread-Safe Singleton)

Manage multiple keys by ID. Useful for multi-agent, multi-tenant, or service-to-service scenarios.

from tenuo import KeyRegistry, SigningKey

registry = KeyRegistry.get_instance()

# Register keys at startup
registry.register("worker", SigningKey.from_env("WORKER_KEY"))
registry.register("orchestrator", SigningKey.from_env("ORCH_KEY"))

# Retrieve by ID
key = registry.get("worker")

# Multi-tenant: namespace keys per tenant
registry.register("api", tenant_a_key, namespace="tenant-a")
registry.register("api", tenant_b_key, namespace="tenant-b")
key = registry.get("api", namespace="tenant-a")

Use cases:

  • LangGraph: Keep keys out of state (checkpointing-safe)
  • Multi-tenant SaaS: Isolate keys per tenant
  • Service mesh: Different keys per downstream service
  • Key rotation: Register current and previous keys

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 (current + previous)
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 guard

# Create bound warrant
keypair = SigningKey.generate()  # In production: SigningKey.from_env("MY_KEY")
warrant = (Warrant.mint_builder()
    .tools(["search"])
    .mint(keypair))
bound = warrant.bind(keypair)

# Protect tools
from langchain_community.tools import DuckDuckGoSearchRun
protected_tools = guard([DuckDuckGoSearchRun()], bound)

# Use in agent
agent = create_openai_tools_agent(llm, protected_tools, prompt)

Using @guard Decorator

Protect your own functions with @guard. Authorization is evaluated at call time, not decoration time - the same function can have different permissions with different warrants:

from tenuo import guard

@guard(tool="read_file")
def read_file(path: str) -> str:
    return open(path).read()

# BoundWarrant as context manager - sets both warrant and key
bound = warrant.bind(keypair)
with bound:
    content = read_file("/tmp/test.txt")  # ✅ Authorized
    content = read_file("/etc/passwd")    # ❌ Blocked

# Different warrant, different permissions
with other_warrant.bind(keypair):
    content = read_file("/etc/passwd")    # Could be ✅ if this warrant allows it

LangGraph Integration

from tenuo import KeyRegistry
from tenuo.langgraph import guard, TenuoToolNode, load_tenuo_keys

# Load keys from TENUO_KEY_* environment variables
load_tenuo_keys()

# Wrap pure nodes
def my_agent(state):
    return {"messages": [...]}

graph.add_node("agent", guard(my_agent, key_id="worker"))
graph.add_node("tools", TenuoToolNode([search, calculator]))

# Run with warrant in state (str() returns base64)
state = {"warrant": str(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.allows("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 mint(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)

# ❌ This raises TypeError
pickle.dumps(bound)
json.dumps(bound)

# ✅ Extract warrant for storage (str() returns base64)
state["warrant"] = str(bound.warrant)
# Reconstruct later with Warrant(string)

allows() vs validate()

# ✅ allows() = Logic Check (Math only)
# Good for UI logic, conditional routing, fail-fast
if bound.allows("delete"):
    show_delete_button()

if bound.allows("delete", {"target": "users"}):
    print("Deletion would be permitted by constraints")

# ✅ validate() = Full Security Check (Math + Crypto)
# Proves you hold the key and validates the PoP signature
result = bound.validate("delete", {"target": "users"})
if result:
    delete_database()
else:
    print(f"Failed: {result.reason}")

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

License

MIT OR Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tenuo-0.1.0a10.tar.gz (547.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

tenuo-0.1.0a10-cp38-abi3-win_amd64.whl (2.1 MB view details)

Uploaded CPython 3.8+Windows x86-64

tenuo-0.1.0a10-cp38-abi3-manylinux_2_34_x86_64.whl (2.5 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

tenuo-0.1.0a10-cp38-abi3-macosx_11_0_arm64.whl (2.2 MB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file tenuo-0.1.0a10.tar.gz.

File metadata

  • Download URL: tenuo-0.1.0a10.tar.gz
  • Upload date:
  • Size: 547.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for tenuo-0.1.0a10.tar.gz
Algorithm Hash digest
SHA256 2c9c8d0061ae8c696e2fa17431df5c926f2c6f784c70875547dd949a33f4310a
MD5 abe069133564d848af2fda7fe74993bb
BLAKE2b-256 46524bb774e5a3f65996aecda7ddfa3bbc9fc858f10f12b6e570e58174f821ff

See more details on using hashes here.

File details

Details for the file tenuo-0.1.0a10-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: tenuo-0.1.0a10-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

Hashes for tenuo-0.1.0a10-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 908873a06fc4414cd030a83f893d323e885e57ce82abb1c40c0c6605e16c4414
MD5 5d4de24489bbe5d931094e6a600a4565
BLAKE2b-256 4d72161a6c3fdd4ec47a4acb6fdec4b84e561a46a8f8a85d43fd63c061c440c4

See more details on using hashes here.

File details

Details for the file tenuo-0.1.0a10-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for tenuo-0.1.0a10-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 8c3fe82cb74a1d87dc7a67bfce91daed7f3c0356a544e704d83ba9aa87bcb387
MD5 463e1dbb2d73c338727346054d2ad503
BLAKE2b-256 24f53df681c2d068921901b7872f92c026e169ba9312a718171b61bb78c2c5a6

See more details on using hashes here.

File details

Details for the file tenuo-0.1.0a10-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for tenuo-0.1.0a10-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f84b3a178943c43ecb2fee155d04d5e3eeae17f33884c10894ba90afcea7ceec
MD5 daa5ca659d3cf4c11a098ca8cde50d46
BLAKE2b-256 923dc23175a467d657723fc7a20cb23b895ac844f88f38aa4e0b36e92b503f1e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page