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@toolon top of@guard. MapGuardianApprovalPendingto a LangGraphinterrupt. - 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, usemode="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:
- In the Guardian console, connect a tool server.
- Add your function as "My own code (record-only)" (for example
pay_invoice). - Allow it for the agent.
Since 0.1.1,
Guarddoes not try to register tools itself.install_record_transport=Trueturns that back on, but it is denied unless the agent's grant pinsmcp.install. Denials carry ahintwith the next step, for exampleGuardianDenied.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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.