Skip to main content

AGLedger Python SDK

The official Python SDK for AGLedger: change control for AI agents. A self-hosted notary that records every change an agent makes, signed and hash-chained, and gates the ones that matter.

Learn more

  • agledger.ai: what AGLedger is and who needs it
  • How it works walks the lifecycle: Record, Completion, Verdict
  • Glossary: canonical definitions of Record, Completion, SCITT Receipt, Verdict, Settlement Signal
  • Documentation: installation, integration guides, API reference

Install

pip install agledger

Quick Start

import os
from agledger import AgledgerClient

client = AgledgerClient(
    api_key=os.environ["AGLEDGER_API_KEY"],
    base_url=os.environ["AGLEDGER_EXTERNAL_URL"],  # your AGLedger instance URL
)

# Create a Record. An agent key defaults the principal to itself; an admin
# key names the principal explicitly via principal_agent_id.
record = client.records.create(
    type="principal-gate-generic-v1",
    contract_version="1",
    platform="internal",
    performer_agent_id="agt-123",
    auto_activate=True,
    criteria={"summary": "Procure 100 widgets", "amount": 500, "currency": "USD"},
)

# Submit a completion
completion = client.completions.submit(
    record.id,
    evidence={"summary": "Delivered 95 widgets", "evidenceUrl": "/out.pdf"},
)

# Principal verdict
client.records.submit_verdict(record.id, completion_id=completion.id, verdict="accept")

Configuration

client = AgledgerClient(
    api_key="agl_agt_...",                              # or set AGLEDGER_API_KEY env var
    base_url="https://agledger.internal.example.com",   # your instance URL. Required.
    max_retries=3,                                      # default: 3
    timeout=30.0,                                       # default: 30s
    idempotency_key_prefix="my-app-",                   # default: ""
)

base_url is required: every AGLedger deployment is self-hosted, so there is no default server to call. Omitting it raises ConfigurationError at construction, where the mistake is, rather than failing every subsequent call against a host you never named. api_key is the one option that falls back to an environment variable (AGLEDGER_API_KEY).

Async Support

import os
from agledger import AsyncAgledgerClient

async with AsyncAgledgerClient(
    api_key=os.environ["AGLEDGER_API_KEY"],
    base_url=os.environ["AGLEDGER_EXTERNAL_URL"],
) as client:
    record = await client.records.get("rec-123")

Resources

records, completions, gate, disputes, webhooks, reputation, events, schemas, compliance, health, admin (with admin.records + admin.vault sub-resources), a2a, agents, audit (with audit.org_reads_checkpoints and audit.vault_checkpoints), auth, capabilities, discovery, references, federation, federation_admin, verification_keys, scitt (SCITT/SCRAPI entries + Transparency Service keys), predicates (predicate schema discovery).

When two publishers offer the same Type

Importing a peer's manifest (schemas.import_()) can leave your org with two registrations of one type: theirs and your local one. That is supported, and it means a bare type no longer names a schema. The API refuses to guess, because the guess would change the moment the other publisher shipped a higher version:

from agledger import AgledgerClient, UnprocessableError

client = AgledgerClient(api_key="agl_agt_...", base_url="https://agledger.internal.example.com")

try:
    client.records.create(type="acme-po-v1", criteria={"poNumber": "PO-1"})
except UnprocessableError as err:
    if err.type == "/problems/ambiguous-publisher":
        # err.publishers is the candidate list, e.g. ["acme-corp", "local"].
        client.records.create(
            type="acme-po-v1",
            criteria={"poNumber": "PO-1"},
            publisher="acme-corp",
        )

Branch on err.type, not on the message. Schema reads take the same pin (client.schemas.get("acme-po-v1", publisher="acme-corp")), and client.schemas.list() returns one row per (publisher, type) so you can choose before reading.

Every Record reports the binding the engine used, whether or not you pinned it:

record = client.records.get(record_id)
record.publisher   # "acme-corp", or None (see below)
record.schema_url  # "/v1/schemas/acme-po-v1?publisher=acme-corp". Follow it verbatim.

publisher is None on Records the engine never validated against a local registration: federation-received ones (the originator ran the gate against its own registration) and ones backfilled through admin import. Read that as "ask the originator", not as "the schema is missing here".

Single-publisher orgs, which is nearly every install, never pass publisher and read their one label (usually local) back.

Webhook Verification

Webhooks ship in two signing schemes, selected per subscription via signing_alg.

HMAC (signing_alg="hmac", the default) is shared-secret HMAC-SHA256:

from agledger.webhooks import verify_signature

is_valid = verify_signature(raw_body, request.headers["x-agledger-signature"], webhook_secret)

Asymmetric (signing_alg="ed25519" or "ecdsa-p256-sha256") is RFC 9421 HTTP Message Signatures signed with the Server's vault key. The receiver holds no secret and verifies against the Server's published public key, giving non-repudiation for the Settlement Signal hop. Settlement-event subscriptions default to this when the Server has a vault signing key. The wire alg reflects the Server's active key; verify_rfc9421 handles both.

from agledger.webhooks import verify_rfc9421, SignatureAlgorithmUnavailableError

# Resolve the Server's published keys once (cache them); the delivery's
# keyid is matched against them automatically.
keys = client.verification_keys.list().data

try:
    is_valid = verify_rfc9421(
        request.headers,  # must include content-digest, signature-input, signature, x-agledger-idempotency-key
        raw_body,
        keys,             # or a single base64 public key string
    )
    if not is_valid:
        return Response(status=401)
except SignatureAlgorithmUnavailableError:
    # This host cannot compute the algorithm, so nothing was checked. Your
    # configuration, not the sender's: 401 would blame the wrong party.
    return Response(status=500)

verify_rfc9421 recomputes the RFC 9530 Content-Digest, reconstructs the RFC 9421 signature base, verifies the signature under the algorithm the resolved key commits to (Ed25519 or ES256), and enforces the created replay window (default/max 300s). construct_event_rfc9421 verifies and parses in one step. This path needs the cryptography extra (pip install 'agledger[verify]').

If the host runtime cannot compute the key's algorithm, both functions raise SignatureAlgorithmUnavailableError instead of returning False. The usual cause is an active OpenSSL FIPS provider, which carries no EdDSA. This is deliberately not a verification failure: returning False would make the standard if not ok: return 401 reject every legitimate delivery as forged, when the fault is in the receiver's configuration rather than the sender's signature. Terminate the signature on an unrestricted host, or configure the sender for ecdsa-p256-sha256, which FIPS does permit.

Note that on such a host no delivery can be classified, valid or forged. The check has to run before signature verification, so a genuine forgery raises too. Treat the exception as "nothing is known about this delivery", never as evidence it was legitimate.

Offline Audit Export Verification

Verify a Record's hash-chained, signed audit export without calling the API:

from agledger.verify import verify_export

export_data = client.records.get_audit_export("rec-123")
result = verify_export(export_data.model_dump(by_alias=True))

if not result.valid:
    print(f"Broken at position {result.broken_at.position}: {result.broken_at.code}")
# VerifyExportResult(valid=True, verified_entries=12, total_entries=12, ...)

broken_at.code is a canonical SCREAMING_SNAKE FailureCode (e.g. CHAIN_HASH_MISMATCH, CHAIN_SIGNATURE_INVALID) shared with the TypeScript verification core, so both languages report identical verdicts over the shared conformance corpus.

Requires cbor2 (for COSE_Sign1 decoding) and cryptography (for signature verification):

pip install 'agledger[verify]'

Decodes canonical COSE_Sign1 envelopes (RFC 9052), walks the hash chain, and verifies each signature under the algorithm the verification key commits to (Ed25519 or ES256). Format 2.0 (1.0 was JCS + detached Ed25519). Pass public_keys={...} to supply out-of-band keys (these override the export's embedded keys), require_key_id="key-id" to reject exports signed by an unexpected key, or require_out_of_band_keys=True for a high-assurance audit that refuses the export's own embedded keys. result.key_provenance reports how many signatures were checked against out-of-band vs embedded keys.

On a FIPS-locked host there is no EdDSA, so an Ed25519 chain cannot be verified there (ES256 chains can). That is reported as CHAIN_UNSUPPORTED_ALGORITHM, never as a signature failure: "I could not check this" and "I checked this and it failed" lead to opposite conclusions, and only one is grounds for a tamper investigation. The result still fails closed. To verify an Ed25519 chain, re-run on a host without the restriction; verification is entirely offline, so the export and keys are portable.

Offline Full-Vault Dump Verification

For a whole-instance audit (not just one Record), verify a five-file NDJSON dump produced by the API's dump-vault tool. This walks every per-record and per-org schema-event chain, cross-checks the signed vault checkpoints against the live chain, and verifies the org_admin_reads Merkle log + signed tree heads (including fork detection):

from agledger.verify import load_dump, verify_dump

report = verify_dump(load_dump("./vault-dump-dir"))
if not report.ok:
    for f in report.vault.failures + report.org_admin_reads.failures:
        print(f"[{f.code}] {f.message}")

agledger-verify CLI (turnkey)

The [verify] extra installs an agledger-verify console script that auto-detects its argument: a directory is a full-vault dump, a file is a single /audit-export JSON document, so one command covers both verifiers, with no network calls:

pip install 'agledger[verify]'

agledger-verify ./vault-dump-dir              # full-vault dump
agledger-verify audit-export.json             # single record export
agledger-verify ./vault-dump-dir -f json      # machine-readable report
agledger-verify ./vault-dump-dir --quiet      # exit code only

Exit codes: 0 clean, 1 verification failure, 2 usage/IO error (so a missing file is never mistaken for tamper). Every failure carries an actionable next step via agledger.verify.suggestion(code). The dump verifier emits the same canonical FailureCode taxonomy as the TypeScript @agledger/verify and is held to the same shared conformance corpus, so the two agree verdict-for-verdict.

SCITT / SCRAPI

Register Signed Statements with the Transparency Service and retrieve Transparent Statements (Signed Statement + Receipt(s)):

receipt = client.scitt.entries.register(signed_statement)
# COSE_Sign1 Merkle inclusion proof per draft-ietf-cose-merkle-tree-proofs-18

transparent = client.scitt.entries.get(entry_id)
# Transparent Statement: Signed Statement with one or more Receipts embedded

keys = client.scitt.keys.list()
# COSE_KeySet of the Transparency Service's signing keys

Wire format is binary application/cose. Errors surface as RFC 9290 CBOR problem-details on APIError.raw_body.

Predicate Schemas

Fetch the canonical JSON Schemas for each predicate kind (record-state, settlement-signal, vault-checkpoint, schema-event, org-read, counter-attestation, federation-projection):

kinds = client.predicates.list()
schema = client.predicates.get("settlement-signal")

Attestation Export

Pull a Record's chain as a tagged COSE_Sign1 stream or a sigstore-bundle v0.3.2 projection for Rekor / in-toto / sigstore-policy-controller ingest:

cose_sequence = client.records.get_attestation(record_id)
# application/cose-sequence bytes (tagged COSE_Sign1 stream)

bundle = client.records.get_attestation_bundle(record_id)
# sigstore-bundle v0.3.2 projection

Vault Checkpoints

Per-record signed Merkle anchors are emitted every 6 hours, letting an auditor detect audit-vault TRUNCATE / DELETE tampering offline:

checkpoints = client.audit.vault_checkpoints.list(record_id="rec-123")

Licensing

The database is the license line. AGLedger is free with its bundled PostgreSQL (Docker Compose or Helm), in production, with every feature and every topology, federation included. Connecting to an external or managed database (Aurora, RDS, Cloud SQL, self-managed) requires a perpetual Enterprise license, priced per external database instance, plus an annual subscription for Enterprise-grade support. The license is perpetual: production never stops due to licensing.

Full details: agledger.ai/pricing | License Agreement

SDK License

Proprietary. Copyright (c) 2026 AGLedger LLC. All rights reserved.

Download files

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

Source Distribution

agledger-1.8.0.tar.gz (205.1 kB view details)

Uploaded Source

Built Distribution

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

agledger-1.8.0-py3-none-any.whl (118.5 kB view details)

Uploaded Python 3

File details

Details for the file agledger-1.8.0.tar.gz.

File metadata

  • Download URL: agledger-1.8.0.tar.gz
  • Upload date:
  • Size: 205.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agledger-1.8.0.tar.gz
Algorithm Hash digest
SHA256 0c333a980338a5d05da12938c551bb067555f76ceb7b0d6070e125b9b5656781
MD5 3a5124d1c9355d11a8d6daee0efe86ba
BLAKE2b-256 5b64c86e8b656b9d2e9ac3c83ea5538f630d71b8812fd9a7ebfb8b14f705a501

See more details on using hashes here.

Provenance

The following attestation bundles were made for agledger-1.8.0.tar.gz:

Publisher: release.yml on agledger-ai/sdk-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agledger-1.8.0-py3-none-any.whl.

File metadata

  • Download URL: agledger-1.8.0-py3-none-any.whl
  • Upload date:
  • Size: 118.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agledger-1.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 41069ca3a420a46ab72b3b339aa965a56a9651135e97d4d6d4310b3df3021c45
MD5 25af07a380fd4a04881106c4d17c6e65
BLAKE2b-256 04fd9bb67378ed7a4d6917ab3410c12fd9c785564eaec81e675cd207f78e540f

See more details on using hashes here.

Provenance

The following attestation bundles were made for agledger-1.8.0-py3-none-any.whl:

Publisher: release.yml on agledger-ai/sdk-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.9.0

2 files

This release

1.8.0 This release

2 files

1.7.0

2 files

1.6.1

2 files

1.6.0

2 files

1.5.1

2 files

1.5.0

2 files

1.4.0

2 files

1.3.0

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.0

2 files

1.0.4

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

0.8.15

2 files

0.8.14

2 files

0.8.13

2 files

0.8.12

2 files

0.8.11

2 files

0.8.10

2 files

0.8.9

2 files

0.8.8

2 files

0.8.7

2 files

0.8.6

2 files

0.8.5

2 files

0.8.4

2 files

0.8.3

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

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