Skip to main content

Matimo AGDK

Matimo Agent Governance Development Kit. Govern any agent in three lines:

from matimo_agdk import Governor

governor = Governor.from_env()
governor.start()

with governor.run("my-run"):
    result = governor.guard(my_tool_fn, name="search")(query="roaiq")

Matimo AGDK is the Python SDK half of Matimo Gateway (the other half is a zero-code LLM proxy any HTTP client can point at directly, see the base_url-swap section below). AGDK adds three things a proxy alone cannot: a cryptographic agent identity signed locally, mandatory execution telemetry so an unregistered or gone-quiet agent is provably absent (not just unobserved), and a governed tool-call check-in with human-in-the-loop support.

Install

Not on PyPI yet: until it is, install from a checkout of this repository (pip install -e ".[all]" or uv sync --all-extras). The commands below show the intended PyPI form. Python 3.13 or newer is required.

pip install matimo-agdk

Framework adapters are optional extras -- see "Framework adapters" below for what each one actually does:

pip install matimo-agdk[langchain]
pip install matimo-agdk[google-adk]
pip install matimo-agdk[crewai]
pip install matimo-agdk[autogen]
pip install matimo-agdk[all]

The full guide, from getting an API key to operating an agent, is docs/USER-MANUAL.md.

Quickstart

  1. Register the agent once (per machine, not per run) and get an org API key from your Matimo Enterprise tenant admin:

    export MATIMO_API_KEY=me-live-...
    matimo-agdk register --name my-agent --framework custom
    

    This writes ~/.matimo/agents/my-agent.json (identity metadata) and ~/.matimo/agents/my-agent.pem (the private key, 0600 on POSIX). The private key is returned by the server exactly once, at registration -- there is no way to retrieve it again. Losing it means registering fresh, or rotating the key if you still hold the identity and the org API key (matimo-agdk rotate-key --name my-agent). register refuses to run again for a name that already has credentials (a second registration would create a second identity and destroy the first one's key); pass --force only when you really mean to replace them.

  2. Build a Governor from the persisted credentials:

    from matimo_agdk import Governor
    
    governor = Governor.from_env(agent_name="my-agent")
    governor.start()  # starts the background telemetry/heartbeat exporter
    
  3. Wrap the tools you want governed, and record LLM/tool activity inside a run:

    def search(query: str) -> str: ...
    
    
    with governor.run("answer-a-question"):
        result = governor.guard(search, name="search", category="web")(query="roaiq")
        governor.llm_span(model="gpt-4o-mini", duration_ms=812)
    
  4. Point your LLM client at Gateway instead of the real provider. Two ways, depending on whether you need per-request signing:

    import openai
    
    # Quick: attaches the session token once, when the client is built. It is
    # not refreshed (a session lasts one hour by default) and nothing is
    # signed, so it stops working when the session expires, and it never works
    # for an identity whose tenant enforces requireSignedRequests. Fine for a
    # short script; not for a long-running agent.
    client = openai.OpenAI(**governor.openai_client_kwargs())
    
    # Recommended: attaches a live session token and signs every request, and
    # transparently re-handshakes if the session expires mid-run. Signing costs
    # nothing when the tenant does not enforce it.
    client = openai.OpenAI(
        base_url=governor.config.base_url,
        api_key=governor.config.api_key,
        http_client=governor.httpx_client(),
    )
    

    For Anthropic, use governor.anthropic_http_client() instead of httpx_client(): anthropic 1.6 and newer are built on httpx2 and reject an httpx.Client. The helper returns whichever client the installed release accepts (pip install httpx2 if it is missing).

    default_headers= alone cannot carry a per-request signature: the signature covers the exact bytes of each request's own body, which is only known once that specific request is being sent. governor.httpx_client() solves this with a request event hook that computes and attaches a fresh Matimo-Agent-Signature per call.

  5. Async code uses the same shape with AsyncGovernor and await:

    from matimo_agdk import AsyncGovernor
    
    governor = await AsyncGovernor.from_env(agent_name="my-agent").start()
    async with governor.run("answer-a-question"):
        result = await governor.guard(async_search, name="search")(query="roaiq")
    

Framework adapters

Every adapter supports mode="observe" (telemetry only, never blocks) and mode="govern" (default: also enforces DENY/PENDING and rapid suspend). Each has one obvious registration point, but which piece actually enforces a DENY differs by framework -- read each module's own docstring before assuming attaching a callback/plugin alone is enough.

Framework Register once LLM through Gateway Enforcement point Signing on LLM calls Verified against
LangChain MatimoCallbackHandler/AsyncMatimoCallbackHandler (telemetry) + govern_tools() (enforcement) gateway_chat_model(provider="openai"|"anthropic") govern_tools()'s wrapped _run/_arun -- the callback alone can only crash the chain, not deny gracefully (see the module docstring) Full (openai), session-header-only (anthropic) langchain-core 1.6.3, langchain-openai 1.6.2, langchain-anthropic 1.7.2
Google ADK MatimoPlugin(governor) on Runner(plugins=[...]) gateway_model() (real openai SDK, no litellm) MatimoPlugin.before_tool_callback's dict short-circuit -- ADK's own documented graceful-DENY contract Full: live session token, run id, and per-request JWS via governor.httpx_async_client() google-adk 2.9.1, openai 2.54.0
CrewAI govern_crew(crew_or_agents, governor) gateway_llm() The wrapped _run/_arun on each tool Full: live session token, run id, and per-request JWS via CrewAI's transport interceptor (verified live with requireSignedRequests=true) crewai 1.15.22
AutoGen govern_tools(tools, governor) gateway_model_client() The wrapped run() on each BaseTool Full autogen-core/-agentchat/-ext 0.7.5 (modern generation only -- see below)
Any other framework govern(callable_or_tools, governor) build your own client with governor.openai_client_kwargs()/httpx_client() governor.guard() under the hood Depends on your client No framework dependency at all

Sync vs. async governors. gateway_chat_model() (LangChain) and gateway_llm() (CrewAI) build their clients synchronously, so they need a sync Governor; passing an AsyncGovernor raises a TypeError that says so. A sync Governor still serves async framework code (blocking calls are moved to a thread). gateway_model_client() (AutoGen) and gateway_model() (ADK) are the reverse: both need an AsyncGovernor, because their underlying async client needs a live per-request signing hook only AsyncGovernor.httpx_async_client() provides. LangChain's ChatOpenAI is wired with the live sync client only, so its async methods (ainvoke) fall back to a session header fixed at construction and are not signed.

"Session-header-only" means: the session token is attached, but there is no per-request Matimo-Agent-Signature (nonce + body hash) the way LangChain's OpenAI path, AutoGen, and ADK get, because the underlying client (ChatAnthropic, CrewAI's LLM) doesn't expose a request-hook mechanism this SDK can use. Each gateway_*/govern_* helper's own docstring explains exactly why, verified by reading the installed package's source, not assumed. If your tenant might ever enable requireSignedRequests, prefer a path marked "Full" for that traffic.

AutoGen note: pyautogen (the legacy 0.2-style ConversableAgent/ register_function package name) ships an empty __init__.py as of its latest release on PyPI (0.10.0) -- verified directly, not assumed. This adapter targets the actively maintained autogen_core/autogen_agentchat/ autogen_ext generation instead. If you're on the legacy API (now under the community ag2 package), wrap your registered functions with matimo_agdk.adapters.generic.govern() directly -- it works with any plain callable.

Spans and runs, by design. LangChain reports one tool span per call: when a tool is wrapped by govern_tools() the wrapper is canonical (it sees the decision) and the callback handler skips its own span for that call; a tool that is not wrapped still gets the handler's span. A denied call's span is tied to LangChain's run tree (its run and span ids) only when a Matimo callback handler is attached, since that is what tells the wrapper which run it is inside. CrewAI and AutoGen give the SDK no per-crew or per-chat id, so they cannot group a crew or chat into one run on their own: wrap the call in governor.run() (or async with governor.run()), which is the grouping mechanism. LangChain LLM spans carry gen_ai.provider.name (openai, anthropic, google) derived from the client class; an unknown class gives no provider.

See examples/{langchain,google_adk,crewai,autogen}_agent.py for a runnable end-to-end demo of each, and each adapter module's own docstring for the full detail this table compresses.

What AGDK does not do

  • AGDK never executes your tools. governor.guard() checks policy and records telemetry around your own callable; the actual tool code always runs in your process, on your infrastructure. A DENY simply means the wrapped call never happens.
  • AGDK never sees your real LLM provider credentials. Once registered, your agent's LLM traffic goes through Matimo Gateway using Gateway's own session token, not your OpenAI/Anthropic key. Gateway resolves the actual BYOK provider credentials server-side.
  • AGDK is not a zero-code option. If you don't need a cryptographic identity, telemetry, or tool governance, you can point any OpenAI/ Anthropic-compatible client directly at Gateway's base_url with just an API key and skip AGDK entirely -- that "base_url swap" path is real and supported, just without the identity/telemetry/tool-check layer this SDK adds.

Rapid suspend: what it really means

An admin can flip an identity's lifecycle_status to suspended or revoked, or trip an org-wide emergency stop. Two different things happen next, on two different timescales:

  • The next call Gateway itself receives (LLM proxy or tool check) is denied immediately, server-side, regardless of AGDK. Lifecycle status is read fresh from the database on every such call, never from a stale cache. This is real and immediate.
  • AGDK's own governor.state/is_suspended()/raise_if_suspended()/ on_suspend(callback) are POLLED, not pushed. They reflect whatever the last telemetry heartbeat response said, which can lag the true server state by up to one heartbeat interval (sized automatically from the server's reported staleness window, clamped to 15s-5min, or set explicitly via heartbeat_interval). This is why the product calls this "rapid suspend," never "instant kill": a long-running loop that never calls Gateway again and never checks governor.state in between could, in principle, keep running locally for up to that interval even after being suspended. There is no push-based kill channel in v1 -- see docs/SERVER-CONTRACT.md section 10.

When Gateway is unreachable

A tool check needs Gateway. What a tool call does when Gateway cannot answer at all (a connection error, a timeout, a 5xx) is your choice, per agent:

tool_check_failure_mode What happens
fail_closed (default) The tool does not run. guard() raises ToolCheckUnavailable (a GatewayUnavailable). Every adapter returns it as a recoverable tool error, the way it returns a DENY: LangChain a ToolException (an observation when the tool has handle_tool_error=True), ADK an {"error": ...} dict from before_tool_callback, CrewAI and AutoGen a raised exception their own tool loop turns into an error result.
fail_open_bounded The tool runs, but only while Gateway was last heard from within fail_open_max_stale_seconds (default 300, and hard-capped at 300: a larger value is rejected when the config loads). Otherwise it behaves as fail_closed.

"Heard from" means a successful tool check or a telemetry heartbeat (so call governor.start(); without it only successful checks count). A call that ran this way is marked: ToolDecision.degraded is True, and its tool span carries matimo.degraded_mode=true and matimo.degraded_cache_age_seconds.

Circuit breaker. After tool_check_breaker_threshold (default 3) consecutive transport failures the circuit opens for tool_check_breaker_cooldown seconds (default 30) and checks fail fast, without touching the network, instead of waiting out the transport's retries on every call. After the cooldown one probe goes through; any real answer from Gateway closes the circuit, a failed probe re-opens it. The first N failing calls each still wait out the retry budget (about 2 to 4 seconds when the connection is refused, much longer if packets are silently dropped, because each attempt can wait for connect_timeout or read_timeout).

Never softened, whatever the mode:

  • an explicit DENY, and an unrecognized decision (already a DENY);
  • any 4xx: a bad key, a missing scope, a rejected signature, a suspended or revoked agent, a rate limit. These raise as before and do not trip the breaker;
  • a suspended or emergency-stopped state the last heartbeat reported;
  • a PENDING awaiting approval: polling failures raise, and so does an outage during the re-check that follows a PENDING with no resume token.

What fail_open_bounded cannot know. The SDK does not hold your policy and does not replay Gateway's last answer for a tool. Beyond the freshness rule it adds one guard: it will not fail open for a tool whose most recent decision in this process was DENY or PENDING, so a tool that needs approval is never waved through by an outage. A tool it has not checked before is allowed if Gateway was heard from recently. If that is not acceptable for your tools, keep fail_closed. This concerns tool checks only: LLM calls go through Gateway and fail closed with it, and reporting failures are governed separately by fail_open_telemetry.

Configuration

GatewayConfig loads with this precedence, highest wins: explicit kwargs passed to Governor(...) / Governor.from_env(...) > environment variables > the credentials file written by matimo-agdk register.

Env var Purpose
MATIMO_GATEWAY_URL Gateway base URL (default http://localhost:8000/v1)
MATIMO_API_KEY Org API key (Authorization: Bearer ...). It needs three scopes: gateway:proxy (LLM calls), identity:manage (register, rotate-key, JWKS), and agdk:check (tool checks). A key without agdk:check registers fine and then fails on the first guard() with missing the required scope: agdk:check.
MATIMO_IDENTITY_TOKEN Bearer identity token, if not loading from a credentials file
MATIMO_IDENTITY_ID Identity UUID, ditto
MATIMO_TENANT_ID Tenant UUID, ditto
MATIMO_PRIVATE_KEY Private key PEM inline
MATIMO_PRIVATE_KEY_FILE Path to a private key PEM file
MATIMO_AGENT_NAME Agent name (credentials file key, and default displayName)
MATIMO_FRAMEWORK langchain | google-adk | crewai | autogen | custom
MATIMO_TOOL_CHECK_FAILURE_MODE fail_closed (default) | fail_open_bounded; see "When Gateway is unreachable"
MATIMO_FAIL_OPEN_MAX_STALE_SECONDS How stale fail_open_bounded may be, in seconds (default and maximum 300)

Code-only settings on GatewayConfig: tool_check_breaker_threshold (3), tool_check_breaker_cooldown (30 s), and capture_tool_results (False).

Tool results are not sent by default. With capture_tool_results=True, every adapter and guard() put the tool's result (or, when the tool raised, its error text) on the tool span as gen_ai.tool.call.result: redacted the same way as arguments, then cut to 500 characters. Off by default because a result can hold anything the tool read. Google ADK and the LangChain callback handler used to send it unconditionally; that changed, see the CHANGELOG.

CLI

matimo-agdk register --name my-agent --framework langchain
matimo-agdk status --name my-agent
matimo-agdk rotate-key --name my-agent
matimo-agdk doctor --name my-agent

Testing

Three levels, from fastest/no-dependencies to a full live pass against a real Matimo Gateway. Run them in order -- each one proves something the last one couldn't.

1. Unit tests -- mocked network, no server needed

uv sync --all-extras --group dev
uv run pytest -q
uv run ruff check .
uv run mypy matimo_agdk

No test touches the network: HTTP is mocked with respx or in-process fakes (see CONTRIBUTING.md), so this proves the client builds correct requests and handles every scripted response shape correctly -- signing, retries, redaction, adapter wiring -- without needing Gateway, Postgres, or a license. This is the check to run on every change, and the one CI runs.

tests/contract/ holds the contract test: an OpenAPI 3.1 description of the /v1 routes the SDK calls (gateway-v1.openapi.json), plus recorded-shape response fixtures. The tests drive the real SDK and assert that every request it builds (body, required headers, path parameters) satisfies the spec, that every fixture does too and is parsed into the right result or typed error, and that the SDK cannot build a request the server would reject (over-long fields, a batch over 500 events, values that are not JSON). The spec and fixtures are derived by hand from the server's Zod schemas, not generated or recorded from a live Gateway: they are only as current as their last review against gateway.ts. The canonical, published spec (BUILD-PLAN D16) is a Universal-AgentForge follow-up; when it exists it should replace this file.

2. CLI sanity check -- a real Gateway, no code

Once a local Matimo Gateway is reachable (part of the Matimo Workbench backend, default http://localhost:8000/v1) and you have an org API key from a tenant admin:

export MATIMO_API_KEY=me-live-...
export MATIMO_GATEWAY_URL=http://localhost:8000/v1   # default, can omit
matimo-agdk register --name test-agent --framework custom
matimo-agdk doctor --name test-agent

doctor performs a real signed session handshake and a heartbeat, then prints the lifecycle status. doctor: all checks passed with lifecycle_status=active means the identity, signing, and session path are genuinely wired end to end -- the fastest way to answer "is AGDK actually working" without writing any code.

3. Full live check -- every governance path, against a real tenant

scripts/live_check.py runs 15 end-to-end scenarios (register, doctor, heartbeat, telemetry, signature enforcement, a real signed completion, rapid suspend, tool checks ALLOW / DENY / PENDING approved / PENDING rejected, the LangChain adapter, session expiry, key rotation, call log) against a real Matimo Gateway. It never provisions accounts, keys, licenses, or LLM providers: an admin creates those in Matimo Workbench first and the script only consumes them.

export MATIMO_API_KEY=...       # org API key with gateway:proxy, identity:manage, agdk:check
export MATIMO_TENANT_ID=...     # the tenant that key belongs to
export MATIMO_ADMIN_TOKEN=...   # a tenant-admin session JWT, used only for the admin APIs
export MATIMO_GATEWAY_URL=http://localhost:8000/v1   # default
export MATIMO_GATEWAY_MODEL=gpt-4o-mini              # optional; used for raw chat calls
uv run --group dev python scripts/live_check.py
uv run --group dev python scripts/live_check.py --filter "tool check"

The admin token must belong to the same tenant as the key; the script refuses to start otherwise, because every admin API call would silently act on another tenant. DATABASE_URL is optional and only enables three sub-assertions that no admin API exposes yet (last_telemetry_at, raw telemetry event attributes, the runs row); they are skipped without it. The signed completion scenario fails, not skips, when the tenant's configured provider has no working key, because that is a real fact about the tenant.

Development

See CONTRIBUTING.md.

License

MIT, copyright 2026 ROAIQ Technologies. See LICENSE.

Metadata

Release files for matimo-agdk 0.1.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 matimo-agdk 0.1.0
File Size Uploaded
matimo_agdk-0.1.0.tar.gz 505.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for matimo-agdk 0.1.0
File Interpreter ABI Platform
matimo_agdk-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 626.7 kB

Release files / matimo_agdk-0.1.0.tar.gz

Download URL matimo_agdk-0.1.0.tar.gz
Size 505.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4f63fc3beab5b10e2b9188ab71a1229838667e7a7243bf8d9dee2bf96d11613e
BLAKE2b-256 checksum
How to use checksums
668d9b029fa5c5cfb8e4b206786362fbdbd2066428f5a31b939cb77755ac80a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / matimo_agdk-0.1.0-py3-none-any.whl

Download URL matimo_agdk-0.1.0-py3-none-any.whl
Size 121.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0b83fb321bd137eb0fe8a98d4e93179cabd3e62cc10e6e3d0da6c5ce6a727aeb
BLAKE2b-256 checksum
How to use checksums
0df88d67a89ea1e61ebbd23d3136c2c12ab394d7003b2420eb21652e380f8b38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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