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 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

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.0a9.tar.gz (517.9 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.0a9-cp38-abi3-win_amd64.whl (2.1 MB view details)

Uploaded CPython 3.8+Windows x86-64

tenuo-0.1.0a9-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.0a9-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.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

Hashes for tenuo-0.1.0a9.tar.gz
Algorithm Hash digest
SHA256 907e80f518530ba7f7a0a792ae2f1a53b7aaaeb8438a11ebc121025917d138a9
MD5 7bea7b782540737536d83c26cadfcc0d
BLAKE2b-256 02fd0d6fddcf598e663af8c757fc02bfbe8293476a3609ad46f78f90d4a5ac85

See more details on using hashes here.

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

Hashes for tenuo-0.1.0a9-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 bd5fb0fd691ba00f8d360b9983d2446572f59ce294c7d152ca873b084bb87ca2
MD5 f72c32e3802fd29e14f524f36d1c8a04
BLAKE2b-256 6b008ab54aacb2360c2b5d27d032ccf9ca4353ed5f7ef4edd1b91f7fa77bcd5e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for tenuo-0.1.0a9-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 29f2318d8a754eabb119457ed46ad2367535c95aa849e9fa0c5e7212df76d243
MD5 4e0a69c44093ec60c492b6c6d9dd76fe
BLAKE2b-256 8b4e4c1f3c0df5fc092d0ee8861d7d7d9426942f824c4c47617406f48ad6402b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for tenuo-0.1.0a9-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9d9c27e64efd8bb3a6bb8c64b68040df5de08cae23714a8a06f310e83899b33d
MD5 f9d545a57a888fd2978cee485df69745
BLAKE2b-256 e3070e1e111fcfa787ad5f3475881a3729a5f18d95bae61b8ab435e4bcbc7787

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