Skip to main content

Agent Middleware (AMW) Python SDK

Typed async client for the governed MCP trust loop:

discover → authenticate → authorize → invoke → meter → receipt → audit → govern

The amw-sdk distribution is at version 0.5.0. Tags matching python-v* trigger the PyPI release workflow. Until publication, install from this checkout.

Installation

For the supported evaluation, integration, security, and pilot paths, start with the repository documentation guide.

From PyPI after publication

Choose the base package or an extra when amw-sdk is available on PyPI:

pip install amw-sdk
pip install "amw-sdk[verify]"
pip install "amw-sdk[mcp]"

Install the version in this checkout:

python -m pip install -e ./sdk/python

Add offline receipt verification when you need it:

python -m pip install -e './sdk/python[verify]'

The legacy MCP server uses python -m b2a_sdk.mcp serve. Add its dependency when you need that command. The extra pins mcp below 2.x, which renamed the class the server imports:

python -m pip install -e './sdk/python[mcp]'

mcp serve sends each tool call through the gateway's standard POST /mcp tools/call flow. Authenticate with a wallet-scoped B2A_API_KEY; the gateway mints a bounded permit for the call. The tool does not accept permit_id. Supply io.agentmiddleware/idempotency_key in the request _meta using a stable logical call ID, such as the LLM tool_call.id. The bridge forwards it verbatim as Idempotency-Key and refuses keys that are not printable ASCII or have leading or trailing whitespace. A retry with the same idempotency key must return the original receipt instead of dispatching again. The gateway also reuses the original permit for that key. Calls without a caller key get a new permit and cannot be safely retried as the same logical call. Action-bound tools that require trusted issuance return action_permit_required on this flow.

Historical release

Version 0.4.0 was released under python-sdk-v0.4.0 with the b2a_sdk-0.4.0-py3-none-any.whl wheel.

The linked 0.4.0 wheel predates the amw import and CLI. Install this checkout to run the examples below.

httpx is the only runtime dependency. Offline receipt verification additionally needs cryptography, kept behind the verify extra.

Migrating from b2a-sdk

b2a-sdk was never published to PyPI. If you installed it locally or from the 0.4.0 wheel, remove it before installing amw-sdk. Until amw-sdk is on PyPI, install it from a checkout instead:

pip uninstall b2a-sdk
pip install amw-sdk  # once published to PyPI
# before publication, from a repository checkout:
pip install ./sdk/python

Your b2a_sdk imports remain unchanged.

Short tool call

Client.call(tool, args, key=None, permit_id=None, wallet_id=None) is a blocking call through the existing governed MCP route. The gateway requires a wallet and an existing permit. Supply their IDs in call() or when creating Client. An API key alone does not provide either ID.

from amw import Client

with Client(
    "YOUR_DEMO_KEY",
    base_url="https://your-gateway.example.com",
) as client:
    receipt = client.call(
        "partner.echo",
        {"message": "hi"},
        key="order-123",
        permit_id="YOUR_PERMIT_ID",
        wallet_id="YOUR_WALLET_ID",
    )
    result = receipt.verify()
    assert result.ok

call() returns a Receipt with .id, .request_key, and .raw. It also reads the authorized portable receipt and the gateway's published trust keys. verify() uses those held documents offline and returns a VerificationResult. If either read fails, call() still returns the receipt. You can later pass both documents as receipt.verify(bundle=bundle, keys=keys). When result.ok, read authenticated fields from result.claims. For independent issuer assurance, supply keys obtained from a source you trust.

When key is omitted, call() uses request_key(tool, args) and exposes the result as receipt.request_key and client.last_request_key. The client sets last_request_key before dispatch. Keep the key because a retry with the same key returns the original receipt instead of dispatching again. The request key is client metadata and is not a field in the signed receipt JSON. For distinct LLM tool calls, pass each stable tool_call.id as key; the argument-derived default can give separate calls with identical arguments the same key.

Governed tool call

import asyncio
from datetime import datetime, timedelta, timezone
from decimal import Decimal

from amw import AgentMiddlewareClient, PermitRequest


async def main() -> None:
    async with AgentMiddlewareClient(
        api_key="agt-your-api-key",
        base_url="https://your-gateway.example.com",
    ) as client:
        tools = await client.discover_tools()
        tool = next(item for item in tools if item.name == "partner.search")

        permit = await client.create_permit(
            PermitRequest(
                issuer_wallet_id="agt-wallet",
                subject_wallet_id="agt-wallet",
                subject_key_id="key-runtime",
                scopes=[f"tool:{tool.name}:invoke", "billing:charge"],
                allowed_tools=[tool.name],
                max_credits=Decimal("25"),
                expires_at=datetime.now(timezone.utc) + timedelta(minutes=30),
            ),
            idempotency_key="permit-run-001",
        )

        invocation = await client.invoke_tool(
            tool.name,
            {"query": "quarterly risk"},
            wallet_id="agt-wallet",
            permit_id=permit.permit_id,
            idempotency_key="invoke-run-001",
        )

        verification = await client.verify_receipt(invocation.receipt.receipt_id)
        evidence = await client.get_evidence(invocation.receipt.receipt_id)
        assert verification.valid and evidence.valid


asyncio.run(main())

Callers must provide nonblank idempotency keys when creating permits and invoking tools. Reusing a key with a different request raises IdempotencyConflictError.

For governed LLM tool calls, the caller or orchestrator should pass a stable logical operation ID, such as tool_call.id, as idempotency_key. Keep that ID and the full request unchanged for retries. request_key(...) and idempotency_key="auto" derive keys from arguments; use them only when those inputs uniquely identify one intended action. request_key.explain(...) shows which fields the helper uses or drops. See the request keys guide for gateway retry behavior and error shapes.

The client defaults to http://localhost:8000, never a hosted server. Pass base_url explicitly for any other deployment.

Synchronous use

AgentMiddlewareClient is async-only. Pilot scripts with plain blocking code can use SyncAgentMiddlewareClient, which covers the same discover, permit, invoke, receipt, and evidence calls with no event loop of their own:

from amw import PermitRequest, SyncAgentMiddlewareClient

with SyncAgentMiddlewareClient(
    api_key="agt-your-api-key",
    base_url="https://your-gateway.example.com",
) as client:
    tools = client.discover_tools()
    permit = client.create_permit(PermitRequest(...), idempotency_key="permit-run-001")
    invocation = client.invoke_tool(
        "partner.search",
        {"query": "quarterly risk"},
        wallet_id="agt-wallet",
        permit_id=permit.permit_id,
        idempotency_key="invoke-run-001",
    )
    assert client.verify_receipt(invocation.receipt.receipt_id).valid

For concurrent or long-lived work, use the async client directly.

If a remote tool was dispatched but its outcome could not be confirmed, invoke_tool raises DeliveryUncertainError. Its receipt_id identifies the signed, charged uncertainty receipt; the SDK never retries the dispatch.

In-process permit validation (0.5+)

LocalPermitValidator and GovernedEdgeSession let framework integrations verify a permit's Ed25519 signature locally and mirror the server's per-call permit checks in-process (expiry, tool scope, per-tool call caps, and budget when estimated_credits is supplied) with no RPC. The local checks eliminate the server round trip only for calls the cached permit already provably denies; the server's authorize_and_reserve remains authoritative and can still deny a call the local mirror allowed.

Local signature verification needs the Ed25519 verifier, which is an optional dependency kept out of the base install. Without it, GovernedEdgeSession.open() raises PermitDeniedError("permit_verification_unavailable") before the first governed call:

python -m pip install './sdk/python[verify]'
from decimal import Decimal

from amw import AgentMiddlewareClient, GovernedEdgeSession

async with AgentMiddlewareClient(api_key="...") as client:
    session = await GovernedEdgeSession.open(
        client,
        permit_id="pmt-xyz",
        wallet_id="agt-wallet",
    )
    # Locally-denied calls raise PermitDeniedError immediately (no server
    # round trip); allowed calls go through the governed invoke_tool loop.
    result = await session.invoke(
        "partner.search",
        {"query": "quarterly risk"},
        idempotency_key="invoke-run-002",
        # Without estimated_credits the local mirror skips the budget check.
        estimated_credits=Decimal("2"),
    )

See sdk/python/src/b2a_sdk/edge_client.py for the full LocalPermitValidator and GovernedEdgeSession surface.

Verifying a receipt offline

amw.verify_bundle checks a receipt with no server, no database, and no credential. It imports nothing from the middleware application. Use it to audit a receipt handed to you by another agent, or to check your own long after the call.

from amw import key_set_from_document, verify_bundle

# bundle:   GET /v1/receipts/{receipt_id}/portable   (authorized)
# keys:     GET /.well-known/trust-keys.json         (unauthenticated)
result = verify_bundle(bundle, key_set_from_document(keys))

if result.ok:
    print(result.claims["tool"], result.claims["credits_charged"])
elif result.is_tampered:
    print("receipt does not verify:", result.reason)
else:
    print("cannot determine:", result.status.value, result.reason)

That three-way split is deliberate. is_tampered is a verdict on the receipt; UNKNOWN_KEY, MALFORMED, and UNSUPPORTED say only that this verifier could not decide, usually because the key set is stale. Treating them alike turns an outage into a fraud alarm.

The same check from a shell:

amw verify --bundle receipt.json --keys trust-keys.json
# exit 0 verified, 1 forged, 2 undetermined

Pass --issuer https://api.example.com to fetch the key set instead of supplying it, and --expect-issuer to require the bundle to name the origin you meant to audit.

Compatibility

b2a_sdk and b2a-verify-receipt still work as deprecated aliases for amw and amw verify.

B2AClient remains available for existing integrations and emits a DeprecationWarning. Legacy wallet, telemetry, dry-run, decorator, and edge client methods remain available during the 0.4.x transition. New code should use AgentMiddlewareClient and the typed trust-loop methods.

Legacy charge() and @billable calls made without an idempotency key are not replay-safe: retrying one whose response was lost bills the wallet again. Unkeyed charge() calls now emit a UserWarning to say so. Pass charge(..., idempotency_key="..."), or give @billable / @combined an idempotency_key_factory that returns the same caller-owned operation key on every retry, and the server replays the original charge instead of debiting twice. A key reused for a different charge raises IdempotencyConflictError.

@monitored reports only the exception type on error by default; pass capture_traceback=True to also send the exception message and traceback, which can contain secrets.

Build and test

uv build sdk/python --out-dir dist
uv run --directory sdk/python --extra dev pytest
ruff check sdk/python/src sdk/python/tests

Metadata

Release files for amw-sdk 0.5.0

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

Source distribution (sdist)

Source distribution for amw-sdk 0.5.0
File Size Uploaded Provenance Actions
amw_sdk-0.5.0.tar.gz 111.2 kB Without provenance Details for amw_sdk-0.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for amw-sdk 0.5.0
File Interpreter ABI Platform Actions
amw_sdk-0.5.0-py3-none-any.whl Python 3 none any Details for amw_sdk-0.5.0-py3-none-any.whl

Total release size: 186.1 kB

Release files / amw_sdk-0.5.0.tar.gz

Download URL amw_sdk-0.5.0.tar.gz
Size 111.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9d2aef1d295b7abde62a19646c64cdc2745e782245a0381b9122d1b5cc062d28
BLAKE2b-256 checksum
How to use checksums
bdd3c51ed2b02f986f6907037b4fcc124eba968ced3d865ec2ada5115584d46d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.9

Release files / amw_sdk-0.5.0-py3-none-any.whl

Download URL amw_sdk-0.5.0-py3-none-any.whl
Size 74.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b1afd25d9d38c67e5942b2371ce9db72dbaad9b1cbacc3b5b980507ff45ea2b7
BLAKE2b-256 checksum
How to use checksums
202ffa9b77e1e1189c29d3a1656ba5b072745990544cb5fe2306925a3785eb21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.9

Security for amw-sdk 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files No provenance

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