Skip to main content

odingard-guardian: Guardian SDK for Python

Human Authority Infrastructure for Autonomous AI. The agent proposes and the human decides. Every action your agent wants to take gets a deterministic ALLOW / ASK / DENY decision from Guardian before it runs. ASK waits for a human, and every execution produces a tamper-evident receipt you can verify offline.

Hosted Guardian: guardian.odingard.com. The free tier needs no card.

pip install odingard-guardian

Python 3.9+. Dependencies: httpx, cryptography.

Govern your own action code: Guard

Guardian does not need to ship a connector for your provider. Wrap the function that performs the action (your Stripe call, your WhatsApp send, your robot command, your database write) and Guardian decides whether it may run.

import os
from odingard_guardian import (
    Guard, GuardianClient, GuardianApprovalPending, GuardianDenied,
)

client = GuardianClient(credential=os.environ["GUARDIAN_AGENT_CREDENTIAL"])  # gac1.… per agent
guard = Guard(client, connection_id=os.environ["GUARDIAN_CONNECTION_ID"])

@guard("pay_invoice")
def pay_invoice(invoice_id: str, amount_cents: int, payee_id: str):
    return stripe.PaymentIntent.create(amount=amount_cents, currency="usd", ...)  # your code

try:
    pay_invoice("INV-1002", 1_240_000, "acme-supplies")
except GuardianApprovalPending as held:      # ASK: nothing ran
    notify_approver(held.request_id)
    ...                                      # after the human approves:
    result = held.resume()                   # runs your function, records the receipt
except GuardianDenied as denied:             # DENY: your function never ran
    log.warning("blocked: %s", denied.reason_code)
Decision What happens to your function
ALLOW Runs once. Then the capability is redeemed and the receipt recorded (on_receipt= callback).
ASK Does not run. GuardianApprovalPending.resume() runs it after approval; before approval it raises pending again.
DENY Never runs. GuardianDenied.reason_code says why. An unreachable Guardian is a DENY (fail closed).

The proposal carries the tool name plus the call's bound arguments ({"invoice_id": …, "amount_cents": …, "payee_id": …}). The arguments are bound into the action and the receipt, so an approval does not carry over to changed arguments. Hosted Guardian decides Allow or Ask per tool today. Amount thresholds and payee rules on argument values are not evaluated yet; the invoice example shows them only under the local demo policy.

async

from odingard_guardian import AsyncGuardianClient, Guard

guard = Guard(AsyncGuardianClient(credential=...), connection_id=...)

@guard("send_whatsapp")
async def send_whatsapp(to: str, body: str): ...

await send_whatsapp("+15550100", "Your invoice is ready")
# held.resume() is awaitable for an async guard

Give the decision back to the model instead of raising

guard = Guard(client, connection_id=..., on_ask="result", on_deny="result")
# ASK/DENY now return {"guardian": {"decision": "ASK", "request_id": …, "message": …}}
# so a framework that turns tool output into model context can tell the model what happened.

Framework recipes

@guard(...) returns an ordinary function, so register the guarded function wherever your framework expects a tool:

  • LangChain / LangGraph: StructuredTool.from_function(pay_invoice), or @tool on top of @guard. Map GuardianApprovalPending to a LangGraph interrupt.
  • OpenAI Agents SDK: function_tool(pay_invoice).
  • CrewAI: @tool("Pay invoice") on top of @guard.
  • Claude Agent SDK / MCP: guard the handler, or put any MCP server behind guardian-mcp proxy (see the MCP package).

Honest limits

  • In-process mode is decision-gated. Your function runs after ALLOW and before the capability is redeemed. A revocation in that window is detected after the effect and raised as GuardianExecutionFailed(local_effect_ran=True). Treat that as an incident. Where Guardian itself must perform the call, use mode="remote" with a tool registered in your Guardian connection.

  • Guardian governs what goes through it. An agent or app that never calls Guardian is not governed by it.

  • First-run setup:

    1. In the Guardian console, connect a tool server whose tools JSON lists your function as record-only: [{"name":"pay_invoice","transport":"record"}].
    2. Allow it for the agent.
    3. Pass install_record_transport=False.

    Otherwise the first use proposes an mcp.install registration. That is denied unless the agent's grant pins mcp.install, and a denied registration blocks the call (fail closed). If your policy holds the registration for approval, resume() completes it and then runs the original call.

Lower-level client

from odingard_guardian import GuardianClient, classify_request_result

client = GuardianClient(credential=..., base_url="https://guardian-api.odingard.com")
result = client.request({
    "action": "tool.invoke",
    "connection_id": "con_…",
    "tool_name": "refund_order",
    "arguments": {"orderId": "ord_123"},
})
d = classify_request_result(result)
if d.decision == "ALLOW":
    client.execute(request_id=d.request_id, capability_token=d.capability_token)
elif d.decision == "ASK":
    ...  # keep d.request_id; client.claim(d.request_id) after the human approves

The request goes to client.request, client.claim, client.execute, client.receipts and client.portable_receipt. It is sent as the same canonical action proposal the TypeScript SDK sends.

Verify a receipt offline

No account, credential or Guardian service is needed once you have the published keys:

from odingard_guardian import fetch_receipt_verification_keys, verify_receipt_offline

keys = fetch_receipt_verification_keys("https://guardian-api.odingard.com")["keys"]
receipt = client.portable_receipt(request_id).receipt   # client: your GuardianClient
verify_receipt_offline(receipt, keys)
# {"valid": True, "decision": "ALLOW", "effect": "verified", "subject": {...}}
# or {"valid": False, "reasonCode": "SIGNATURE_INVALID" | "EVIDENCE_CHAIN_INVALID" | ...}

The verifier checks the Ed25519 signature over the guardian-canonical-json.v1 body, then re-derives every evidence record's hash and link, the authority lifecycle order, and every embedded object (decision, approval, capability, execution, effect). Its results and reason codes match the TypeScript SDK on the golden fixtures in tests/fixtures/ (generated by scripts/gen-fixtures.ts). The receipt envelope is schema-checked at the top level. Nested records are covered by the signature and digest re-derivation rather than a field-by-field schema.

Development

cd packages/python
uv venv && uv pip install -e ".[test]"
.venv/bin/python -m pytest
# regenerate cross-language fixtures (from the repo root, after npm install):
npm run fixtures:python

Apache-2.0.

Metadata

Release files for odingard-guardian 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 odingard-guardian 0.1.0
File Size Uploaded Provenance Actions
odingard_guardian-0.1.0.tar.gz 42.2 kB Without provenance Details for odingard_guardian-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for odingard-guardian 0.1.0
File Interpreter ABI Platform Actions
odingard_guardian-0.1.0-py3-none-any.whl Python 3 none any Details for odingard_guardian-0.1.0-py3-none-any.whl

Total release size: 72.7 kB

Release files / odingard_guardian-0.1.0.tar.gz

Download URL odingard_guardian-0.1.0.tar.gz
Size 42.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b37504ecf217fc1bc6b0f5d61a86beeaf12a123327a93fe004ab5112da3f9b1d
BLAKE2b-256 checksum
How to use checksums
f0b3847214ed43485a060dce4ee76b9aaeaaa1d6365bee93f5e211cf244057d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.9

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

Download URL odingard_guardian-0.1.0-py3-none-any.whl
Size 30.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
83affd8f416a6b9582e30485ba14c876a83f2502b9b0abd3f3b768ede480327b
BLAKE2b-256 checksum
How to use checksums
aedc7355d91d1576e210d3d5238e39a3e1af0b77d39fc2b75725197a45f384b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.9

Security for odingard-guardian 0.1.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.

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