Quickstart updated 2026-05-22

Python SDK quickstart

From `pip install cullis-sdk` to a Mastio-authenticated agent. Provision once, run forever — copy-paste runnable.

Python SDK quickstart

Scope of this page: enrollment + transport-layer authentication. For LLM completions and MCP tool calls, see the companion pages Chat completion via Mastio and MCP tools via Mastio. The reference agent in agent_kyc_screener/main_stack.py (cullis-enterprise repo) shows the full loop end-to-end.

Python only. TypeScript / Go / Java SDKs are not available publicly.

Who this is for: a Python developer writing an agent that authenticates to a Cullis Mastio (org gateway) and makes LLM completions + MCP tool calls through it.

The flow is two phases that happen at different times by different people:

  1. Provisioning (one-time, operator). Mastio mints or accepts a cert+key+DPoP key for the agent. Files end up on disk. If someone in your team already handed you the three files, skip to step 3.
  2. Runtime (every agent start, agent itself). Agent reads those files and authenticates to Mastio via mTLS.

Need a Mastio to talk to? A single-host Docker bundle gives you https://localhost:9443 in two commands — see Install Mastio on Docker. The rest of this page uses https://mastio.acme.corp as a placeholder; substitute the host you actually reach.

1. Install

pip install cullis-sdk

Python 3.10+. No required system dependencies. Linux, macOS, Windows supported.

Pin to a released minor in your pyproject.toml so a breaking minor bump doesn’t surprise a future-you:

cullis-sdk = ">=0.2,<0.3"

For SPIRE/SPIFFE workload-API integration (only if you provision via SPIRE — section 2c), install the extra:

pip install 'cullis-sdk[spiffe]'

2. Provision an identity (one-time, operator)

The agent’s runtime identity is three files:

  • cert.pem — x509 client cert, with SAN like spiffe://acme.corp/orga/<agent-name>
  • agent-key.pem — private key matching the cert
  • dpop.jwk — EC P-256 private key bound to the cert thumbprint (DPoP, RFC 9449 — a JWT that proves the request comes from the holder of the matching private key, not just a token bearer)

A note on agent IDs. Cullis identifies agents as <org-id>::<agent-name> (e.g. orga::kyc-screener). Both the raw HTTP admin API and the SDK take only the short agent_name — the Mastio scopes it to its own org_id and returns the full agent_id in the response. You never pass the <org>:: prefix yourself.

A note on capabilities. Free-form strings. There is no closed vocabulary. The convention is <area>.<verb> (e.g. kyc.read, kyc.submit, portfolio.place_order) but Mastio does not validate the syntax at enrollment. What matters is that the strings here match exactly the capabilities required by the MCP tools the agent will call — Mastio’s capability gate compares them literally when a tool is dispatched. Each MCP tool declares its required capability in its server manifest; ask the team that operates the MCP server for the list, or list them at runtime with client.list_mcp_tools().

A note on X-Admin-Secret. It’s the Mastio operator secret. First-boot wizard bcrypts it into Vault; the env var (MCP_PROXY_ADMIN_SECRET) is consulted only when the hash is empty. Rotation is via dashboard / proxy.env rotate + restart. Full details: Configuration reference (search MCP_PROXY_ADMIN_SECRET) and Runbook § Admin lockout for the recovery flow.

Pick one of the three provisioning paths depending on whether your org already has a PKI.

a. Mastio mints the cert (no existing PKI)

If your org doesn’t have a Certificate Authority yet, let Mastio mint everything from its org-scoped CA. Run this once from an operator script:

# One-off, from your operator workstation or a CI/CD provisioning step.
# Self-signed Org CA (the bundle default)? Add --cacert ./certs/org-ca.pem
# to the curl, or -k to skip verification (dev only).
curl -X POST https://mastio.acme.corp/v1/admin/agents \
  -H "X-Admin-Secret: $MASTIO_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "agent_name": "kyc-screener",
        "display_name": "KYC Screener",
        "capabilities": ["kyc.read", "kyc.submit"]
      }' \
  > kyc-screener.json

# The response carries cert_pem (leaf), cert_chain_pem (leaf + Mastio
# Intermediate), and private_key_pem (the freshly minted key). Persist
# to disk. Write the fullchain into cert.pem so strict TLS clients can
# build leaf -> Intermediate -> Org Root; the `// .cert_pem` fallback
# covers legacy single-tier deployments where cert_chain_pem is null.
jq -r '.cert_chain_pem // .cert_pem' kyc-screener.json > /etc/cullis/agents/kyc/cert.pem
jq -r '.private_key_pem'             kyc-screener.json > /etc/cullis/agents/kyc/agent-key.pem
chmod 0600 /etc/cullis/agents/kyc/*

# No dpop.jwk here — the curl mint path doesn't produce one (unlike the
# SDK enroll_via_* helpers in paths b/c below, which persist it). While
# the Mastio's egress_dpop_mode is `optional` (the bundle default) the
# agent authenticates with mTLS alone, so omit dpop_key_path at runtime.
# For DPoP binding, enroll via the SDK or register a public JWK via
# POST /v1/admin/agents/<agent_id>/dpop-jwk.

Mastio pins the cert thumbprint in its DB. From this moment any TLS handshake presenting that exact cert authenticates as orga::kyc-screener.

Your organisation already runs a CA (HashiCorp Vault, AD CS, EJBCA, AWS Private CA, whatever). The agent has a cert + private key signed by it. You hand both to Mastio once; Mastio verifies the chain and pins the thumbprint.

import os
from pathlib import Path
from cullis_sdk import CullisClient

# Operator-side script, run once per agent at provisioning time
with open("/path/to/agent.pem") as f:
    cert_pem = f.read()
with open("/path/to/agent-key.pem") as f:
    private_key_pem = f.read()

CullisClient.enroll_via_byoca(
    "https://mastio.acme.corp",
    admin_secret=os.environ["MASTIO_ADMIN_SECRET"],   # ← admin privilege
    agent_name="kyc-screener",                         # org prefix auto-prepended
    display_name="KYC Screener",
    cert_pem=cert_pem,
    private_key_pem=private_key_pem,
    capabilities=["kyc.read", "kyc.submit"],
    persist_to="/etc/cullis/agents/kyc/",              # writes cert.pem + agent-key.pem + dpop.jwk
)

Mastio verifies the chain against the Org CA you previously attached (see BYOCA enrollment), pins the thumbprint, mints a DPoP key, persists the three files at persist_to using the same filenames the runtime constructor expects.

→ Full chain rules + CA attach flow: BYOCA enrollment.

c. SPIRE — workload identity from a SPIRE agent

Your agent runs in a SPIRE-attested environment (Kubernetes with SPIRE installed). The SPIRE workload API hands the agent an SVID; the SDK exchanges it for cert+key pinned in Mastio.

import os
from cullis_sdk import CullisClient

CullisClient.enroll_via_spiffe(
    "https://mastio.acme.corp",
    admin_secret=os.environ["MASTIO_ADMIN_SECRET"],
    agent_name="kyc-screener",                         # org prefix auto-prepended
    persist_to="/var/lib/cullis/agents/kyc/",          # writes cert.pem + agent-key.pem + dpop.jwk
)

→ Full SPIRE flow: SPIRE enrollment.

3. Runtime — load identity + authenticate (every agent start)

Once the three files are on disk, the agent’s entrypoint is the same regardless of how you provisioned them:

from cullis_sdk import CullisClient

client = CullisClient.from_identity_dir(
    mastio_url="https://mastio.acme.corp",
    cert_path="/etc/cullis/agents/kyc/cert.pem",
    key_path="/etc/cullis/agents/kyc/agent-key.pem",
    dpop_key_path="/etc/cullis/agents/kyc/dpop.jwk",
    ca_chain_path="/etc/cullis/ca/orga-ca.pem",   # to verify Mastio's TLS cert
)
client.login_via_proxy_with_local_key()

Where does ca_chain_path come from? It’s the Org CA cert that signs Mastio’s own TLS cert. Operators export it from the Mastio dashboard (PKI → Export CA Certificate) or fetch it from the bundle’s certs/org-ca.pem — the user-readable copy deploy.sh writes for exactly this (its “Next steps” banner points there too). The sibling nginx-certs/org-ca.crt holds the same cert but is owned by the in-container runtime UID and is not readable as your shell user. Distribute the file alongside the three identity files. If you set verify_tls=False you don’t need it, but that’s for local dogfood only.

Two-line mental model:

  • from_identity_dir(...) is pure local: opens 3 files, builds an httpx client. No network call. If this fails, your error is a FileNotFoundError or bad PEM format.
  • login_via_proxy_with_local_key() is the first network call. mTLS handshake + DPoP challenge + token issue. If this fails, your error is a TLS / 401 / connection error pointing at the Mastio.

Verify it works: if login_via_proxy_with_local_key() returns without raising, you are authenticated. The SDK has cached a short-lived token bound to your cert + DPoP key, and will auto-attach it to every subsequent call. There is no explicit .ping() to run — a successful login is the green light.

What happens under the hood

  1. The SDK opens a TLS handshake to Mastio presenting the agent cert as a client cert (mTLS, RFC 8705 — the TLS-layer counterpart to OAuth client authentication).
  2. Mastio compares the cert’s SHA-256 thumbprint against the one pinned at provisioning. Mismatch → 401.
  3. The DPoP key signs subsequent egress requests, binding the token to the keypair. Mastio rejects replays from other clients.
  4. From here on client has an authenticated session. No admin secret in scope, no enrollment call at startup.

from_identity_dir is the only runtime constructor you need. Production agents that load credentials from Vault, K8s secrets, HSMs, or any other secret store use this same call once the material has been read into the three file paths.

Production: systemd LoadCredential

For Linux production deployments, prefer systemd’s LoadCredential= mechanism over plain 0600 files. systemd materialises the credentials on tmpfs under $CREDENTIALS_DIRECTORY only for the lifetime of the unit, with 0400 root:root perms, gone the moment the service stops. The agent never reads from a writable disk.

Unit file (/etc/systemd/system/cullis-kyc-agent.service):

[Unit]
Description=KYC screener agent (Cullis-authenticated)
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=cullis-kyc
ExecStart=/opt/kyc-agent/venv/bin/python /opt/kyc-agent/main.py

# systemd reads these from disk once at unit start and re-materialises them
# on tmpfs under /run/credentials/cullis-kyc-agent.service/<name>. The
# source paths can be 0400 root:root — only systemd needs to read them.
LoadCredential=cert.pem:/etc/cullis/kyc-screener/cert.pem
LoadCredential=key.pem:/etc/cullis/kyc-screener/key.pem
LoadCredential=dpop.jwk:/etc/cullis/kyc-screener/dpop.jwk
LoadCredential=agent.json:/etc/cullis/kyc-screener/agent.json

[Install]
WantedBy=multi-user.target

Inside the agent, replace from_identity_dir(...) with the zero-argument from_systemd_credentials():

from cullis_sdk import CullisClient

# Reads $CREDENTIALS_DIRECTORY, loads cert + key + dpop, lifts
# mastio_url / agent_id / org_id from agent.json.
client = CullisClient.from_systemd_credentials()
client.login_via_proxy_with_local_key()

The factory accepts the same verify_tls, timeout, and ca_chain_path arguments as from_identity_dir, plus overrides if your unit names the credentials differently (cert_name=, key_name=, dpop_key_name=). Setting dpop_key_name=None skips DPoP loading entirely — only safe while the Mastio’s egress_dpop_mode is off or optional.

The agent.json file ships the runtime metadata the SDK needs to talk to your Mastio:

{
  "mastio_url": "https://mastio.acme.corp",
  "agent_id": "orga::kyc-screener",
  "org_id": "orga"
}

It’s the same JSON the enroll_via_byoca / enroll_via_spiffe helpers persist under persist_to/agent.json, so an operator who provisioned with the SDK can copy it verbatim to /etc/cullis/kyc-screener/. Operators who provisioned with curl write it by hand.

What’s next

The SDK exposes cullis_sdk.__version__ if you need to assert the running version in your own diagnostics.