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.
    2. Add your function as "My own code (record-only)" (for example pay_invoice).
    3. Allow it for the agent.

    Since 0.1.1, Guard does not try to register tools itself. install_record_transport=True turns that back on, but it is denied unless the agent's grant pins mcp.install. Denials carry a hint with the next step, for example GuardianDenied.hint.

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.2

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.2
File Size Uploaded Provenance Actions
odingard_guardian-0.1.2.tar.gz 43.2 kB Without provenance Details for odingard_guardian-0.1.2.tar.gz

Built distribution (wheel)

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

Total release size: 74.4 kB

Release files / odingard_guardian-0.1.2.tar.gz

Download URL odingard_guardian-0.1.2.tar.gz
Size 43.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b8ef0d34c318e82808fadd53a3156c22f2957b0a55c54af2aeeff76c554a916d
BLAKE2b-256 checksum
How to use checksums
d5f63517b36b169f7000567a5be8fb93c2fc5d8751638accb7f7ac830c8cd7eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

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

Download URL odingard_guardian-0.1.2-py3-none-any.whl
Size 31.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
157b6a7a3a854e1b24ab0b40f15dae7f85433f4b5e6f3a1d851ea9f5ed62a4a6
BLAKE2b-256 checksum
How to use checksums
5a96222bed151081ba4fb9f24ead23e1ecc9c579796d35b4d3b7d7ca621260c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Security for odingard-guardian 0.1.2

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