Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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 strings
  • att_chain — ordered delegation lineage (list of JTIs)
  • att_depth — delegation depth (0 = root)
  • att_intent — SHA-256 hex of the original instruction
  • att_tid — task tree UUID shared across the chain
  • att_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.

Metadata

Release files for attest-sdk 0.1.0b5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for attest-sdk 0.1.0b5
File Size Uploaded
attest_sdk-0.1.0b5.tar.gz 39.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for attest-sdk 0.1.0b5
File Interpreter ABI Platform
attest_sdk-0.1.0b5-py3-none-any.whl Python 3 none any Details

Total release size: 75.0 kB

Release files / attest_sdk-0.1.0b5.tar.gz

Download URL attest_sdk-0.1.0b5.tar.gz
Size 39.8 kB
Tags Source
SHA-256 checksum
How to use checksums
add2a0dd93f778ce6e32d8213ec84c018b113029226ecadeff47c85d49516a9a
BLAKE2b-256 checksum
How to use checksums
4a32518c0b804d1ddca7fd48b19f16e05b38d1faac1078bffbec5a85cf384ac9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.0

Release files / attest_sdk-0.1.0b5-py3-none-any.whl

Download URL attest_sdk-0.1.0b5-py3-none-any.whl
Size 35.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf753f135681ba78938583bf6b4ce38bb5288ad0c18fc0292a9e589fb740fcfa
BLAKE2b-256 checksum
How to use checksums
9d69f1198ec64893538f8b8dadb1f4cbdd5131e16e8679e55f8155c5936147e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.0
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page