Python SDK for the Attest cryptographic agent credential service
Project description
attest-sdk
Python SDK for the Attest cryptographic agent credential service.
Attest issues RS256-signed JWTs to AI agents. Each token carries:
att_scope— list of"resource:action"permission stringsatt_chain— ordered delegation lineage (list of JTIs)att_depth— delegation depth (0 = root)att_intent— SHA-256 hex of the original instructionatt_tid— task tree UUID shared across the chainatt_uid— originating human user ID
Install
pip install attest-sdk
# With framework integrations
pip install "attest-sdk[langgraph]"
pip install "attest-sdk[anthropic]"
Basic usage
from attest import AttestClient, IssueParams, DelegateParams
client = AttestClient(
base_url="http://localhost:8080",
api_key="your-api-key",
)
# Issue a root credential
token = client.issue(IssueParams(
agent_id="orchestrator-v1",
user_id="user-42",
scope=["research:read", "gmail:send"],
instruction="Draft and send the quarterly report",
ttl_seconds=3600,
))
print(token.claims.att_tid) # task tree UUID
print(token.claims.att_scope) # ["research:read", "gmail:send"]
# Delegate to a child agent (scope must be a subset)
child = client.delegate(DelegateParams(
parent_token=token.token,
child_agent="email-agent-v1",
child_scope=["gmail:send"],
))
# Offline verify — fetch JWKS once, reuse it
jwks = client.fetch_jwks()
result = client.verify(token.token, jwks=jwks) # no server call needed
if result.valid:
print("Issuer:", result.claims.iss)
else:
print("Invalid:", result.warnings)
# Check revocation
is_revoked = client.check_revoked(token.claims.jti)
# Revoke (cascades to all descendants)
client.revoke(token.claims.jti, revoked_by="orchestrator")
# Audit trail for the whole task tree
chain = client.audit(token.claims.att_tid)
for event in chain.events:
print(event.event_type, event.agent_id, event.created_at)
Async client
import asyncio
from attest import AsyncAttestClient, IssueParams
async def main():
async with AsyncAttestClient(api_key="your-api-key") as client:
token = await client.issue(IssueParams(
agent_id="async-agent",
user_id="user-1",
scope=["files:read"],
instruction="Read the file",
))
jwks = await client.fetch_jwks()
result = await client.verify(token.token, jwks=jwks)
print(result.valid)
asyncio.run(main())
LangGraph integration
from typing import TypedDict
from attest import AttestClient
from attest.integrations.langgraph import AttestState, attest_tool, AttestNodes
client = AttestClient(api_key="your-api-key")
# 1. Extend AttestState with your own fields
class MyState(AttestState):
messages: list
instruction: str
user_id: str
# 2. Issue at graph entry — stores JWT in state["attest_tokens"]["orchestrator-v1"]
graph.add_node("issue", AttestNodes.issue(
client=client,
agent_id="orchestrator-v1",
scope=["research:read", "gmail:send"],
instruction_key="instruction",
user_id_key="user_id",
))
# 3. Enforce scope at tool call — raises AttestScopeError if not covered
@attest_tool(scope="gmail:send", agent_id="email-agent-v1")
def send_email(state: MyState, to: str, body: str) -> str:
...
# 4. Delegate when spawning a sub-agent
graph.add_node("spawn_email_agent", AttestNodes.delegate(
client=client,
parent_agent_id="orchestrator-v1",
child_agent_id="email-agent-v1",
child_scope=["gmail:send"],
))
# 5. Revoke at graph teardown
graph.add_node("cleanup", AttestNodes.revoke(
client=client,
agent_id="orchestrator-v1",
))
LangGraph HITL approval
For high-risk handoffs, use AttestNodes.gated_delegate(...) so the graph pauses on a LangGraph interrupt() until a human approves:
from attest.integrations.langgraph import AttestNodes
graph.add_node("approve_prod_deploy", AttestNodes.gated_delegate(
client=client,
parent_agent_id="orchestrator-v1",
child_agent_id="deploy-agent",
child_scope=["deploy:prod"],
intent="Deploy the approved release to production",
))
If you are using AttestStateGraph, you can also require approval declaratively in scope_map:
graph = AttestStateGraph(
MyState,
client=client,
scope_map={
"deploy_node": {
"scope": ["deploy:prod"],
"require_approval": True,
"intent": "Deploy the approved release to production",
},
},
)
Anthropic SDK integration
from attest import AttestClient
from attest.integrations.anthropic_sdk import AttestSession, attest_tool_anthropic
client = AttestClient(base_url="http://localhost:8080", api_key="your-api-key")
with AttestSession(
client=client,
agent_id="claude-orchestrator",
user_id="usr_alice",
scope=["web:read", "files:read", "files:write"],
instruction="Refactor the auth module",
system_prompt=SYSTEM_PROMPT, # auto-computes att_ack checksum
) as session:
@attest_tool_anthropic(scope="web:read")
def search_docs(query: str) -> str:
...
# Delegate narrower scope to a sub-agent
child = session.delegate("code-reviewer", ["files:read"])
run_reviewer(child.token, "src/auth.py")
# Exiting revokes the root credential and all descendants
For approval-gated delegation:
child = session.delegate(
"deploy-agent",
["deploy:prod"],
require_approval=True,
intent="Deploy the approved release to production",
)
This path waits for a human approval challenge to resolve before continuing with the narrowed child credential.
For production-grade HITL provenance in Anthropic flows, pass an approval_handler that exchanges the challenge for the actual approved credential:
child = session.delegate(
"deploy-agent",
["deploy:prod"],
require_approval=True,
intent="Deploy the approved release to production",
approval_handler=lambda challenge: get_id_token_from_your_ui(challenge.challenge_id),
)
Without approval_handler, the session can still wait for external approval, but it resumes by issuing a fresh narrowed credential rather than returning the original HITL-stamped token.
Offline verification note
Once you have fetched the JWKS with client.fetch_jwks(), you can verify
any token from the same server without making additional network calls:
jwks = client.fetch_jwks() # one network call
for token_str in incoming_tokens:
result = client.verify(token_str, jwks=jwks) # pure local crypto
The public key is stable for the lifetime of the server instance; cache it as long as your process runs.
Verify a signed evidence packet
packet = client.fetch_evidence(token.claims.att_tid)
jwks = client.fetch_jwks(packet.org.id)
verified = client.verify_evidence_packet(packet, jwks=jwks)
print(verified.valid)
print(verified.hash_valid, verified.signature_valid, verified.audit_chain_valid)
print(verified.warnings)
This verifier checks the packet hash, the RS256 packet signature, and the append-only audit hash chain.
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 Distribution
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 attest_sdk-0.1.0b5.tar.gz.
File metadata
- Download URL: attest_sdk-0.1.0b5.tar.gz
- Upload date:
- Size: 39.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
add2a0dd93f778ce6e32d8213ec84c018b113029226ecadeff47c85d49516a9a
|
|
| MD5 |
d4d2d5a2c2a8989b885bbf23e9413d53
|
|
| BLAKE2b-256 |
4a32518c0b804d1ddca7fd48b19f16e05b38d1faac1078bffbec5a85cf384ac9
|
File details
Details for the file attest_sdk-0.1.0b5-py3-none-any.whl.
File metadata
- Download URL: attest_sdk-0.1.0b5-py3-none-any.whl
- Upload date:
- Size: 35.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf753f135681ba78938583bf6b4ce38bb5288ad0c18fc0292a9e589fb740fcfa
|
|
| MD5 |
353298c8b778622fa9491612283564cb
|
|
| BLAKE2b-256 |
9d69f1198ec64893538f8b8dadb1f4cbdd5131e16e8679e55f8155c5936147e4
|