decidio (Python)
The one-line approval gate for AI-agent actions — the agent suspends for human approval and resumes, sealing a portable Authority Receipt the customer owns. Decidio gates (proceed | route | block) + records; the agent executes its own action on resume. Decidio never executes and holds no downstream credentials. Python-first, with a TS twin (@decidio/sdk) that emits an identical request + receipt (conformance-asserted).
from decidio import guard
# one line — same surface in every runtime
create_opp = guard.protect(
create_opp_raw,
lambda o: {"action": "createOpportunity", "amount": o["Amount"], "scope": "Opportunity"},
)
# 0.2.0: the call returns what your function returned AND what Decidio recorded about it.
done = create_opp({"Amount": 86_000})
print(done.value) # your function's own return value, untouched
print(done.confirmation["evidence_tier"]) # e.g. "application_confirmed"
# Only want the value? `guard.protect_best_effort(...)` returns it bare — but then "the report
# was not recorded" becomes invisible again, which is the gap this shape exists to close.
- proceed → runs immediately (auto-approved under a named, versioned policy rule), sealed.
- route → suspends (
DecidioSuspended): parks the call args in an agent-side store, the process may exit; resumes when a human approves and re-runs your function. - block → raises
DecidioBlocked; your function never runs.
Setup
pip install 'decidio[signing]'
export DECIDIO_API_URL=https://decidio-api.onrender.com # the hosted sandbox
python -m decidio init my-agent # sign in, register the agent, mint its API token, write .env
Every later command reads .env from the same directory. First run tip: pass
mode="blocking" to guard.protect(...) to watch the whole loop live (trigger → route to a
human → approve with python -m decidio approvals approve <id> → your function executes).
A brand-new agent matches no auto-approve rule, so every request routes to a human —
deny-by-default is the product working, not a misconfiguration.
Other commands: doctor (config + connectivity + token scope), receipt <id> (download the
sealed Authority Receipt). One runtime dependency — cryptography, which signs the request-bound
identity proof and the execution report, so it is a base dependency rather than an extra (without
it a signed agent could not reach a confirming tier at all). Engine adapters are extras.
Durable resume (real approvals take minutes to days)
Without mode="blocking", a routed action suspends: it parks its call args locally and raises
DecidioSuspended; the process may exit. Self-serve transport — start here:
guard.worker() — a durable poll worker that re-executes parked actions on approval, exactly
once. No inbound URL, no shared secrets; this is the transport for the hosted sandbox.
Operator deployments can use the signed webhook instead — Decidio POSTs a verdict to your resume URL and the handler verifies the HMAC fail-closed:
# FastAPI
@app.post("/decidio/resume")
async def decidio_resume(req: Request):
return guard.resume.handle(await req.body(), req.headers.get("x-decidio-signature"))
Honest requirement: webhook signing uses a shared secret configured on BOTH sides — your
DECIDIO_WEBHOOK_SECRET must equal the Decidio server's, and self-hosted production also
allow-lists resume hosts. Against the hosted sandbox, use the worker.
Closing the last window yourself. Pass with_context=True and your action receives an
ExecutionContext as its first argument — {decision_id, attempt_id, idempotency_key}:
pay_invoice = guard.protect(
lambda ctx, inv: stripe.PaymentIntent.create(
amount=inv["amount"], currency="usd",
idempotency_key=ctx.idempotency_key, # stable across every attempt at this decision
),
lambda inv: {"action": "payInvoice", "amount": inv["amount"]},
with_context=True,
)
Opt-in rather than inferred from the signature, because guessing wrong would hand a payment call
a context object where it expected an invoice. describe still receives your arguments only — it
describes the request, while the context describes the execution.
Either way the re-execution guarantee is: once invocation may have begun, the SDK never
automatically invokes it again unless the downstream system provides an idempotency guarantee or
an operator explicitly reconciles it. That holds across processes and restarts — a durable
invoking marker is written to the pending store before your function is called. It is not
exactly-once against an external API, which no client can offer; an action that ran and then
raised becomes indeterminate and waits for reconcile() rather than being retried into a
double-write.
Engine adapters (durable suspend on the engine you already run)
Thin translators onto each engine's native durable wait — pip install decidio[langgraph|temporal|openai]:
# LangGraph — true drop-in (interrupt() is contextvar-based)
create_opp = guard.protect(create_opp_raw, describe, adapter="langgraph")
# Inngest / Temporal / OpenAI Agents — pass the engine handle:
await decidio.adapters.inngest.gate(step, guard, ctx, run=lambda: create_opp_raw(o))
await decidio.adapters.temporal.gate(wf, guard, ctx, run=..., )
resolved, pending = decidio.adapters.openai.gate_interruptions(guard, run_state, describe)
Own the record — verify it yourself
Every outcome is a sealed W3C-VC (Ed25519 did:key), tamper-evident and offline-verifiable with no Decidio dependency:
pip install decidio[verify]
python -m decidio.verify receipt.json
Errors
Every Decidio error subclasses DecidioError, so one except DecidioError: catches all of them.
DecidioBlocked/DecidioRejected— policy blocked it / a human rejected it; your function never ran.DecidioSuspended— durable mode: parked for async approval. Not a failure.DecidioTimeout— blocking mode only: nobody decided in time; the decision stays open, nothing executed.DecidioRateLimitError— the workspace's governed-action limit; carrieslimit/remaining/reset_at.DecidioUnsafeArguments— your call arguments cannot survive JSON with their meaning intact, so nothing was routed and nothing ran. The message names the field and the fix. Most often: aNaNorDecimal, adatetime, a set, or an integer larger than 2^53 — a JSON reader on the other side parses that into a float and rounds it, so the human would approve a different number from the one your function received. Pass those as strings.DecidioHttpError— the gate refused the call. Carriesstatus,endpoint, the parsedbody, andretryable— the field to branch on. A 401/403 is terminal (the agent token expired, or someone re-raninitfor that agent and revoked it): retrying cannot help, so the message names the fix. Anything else is transient, and retrying is safe because a retry creates a new request and can never double-execute.
try:
pay_invoice(inv)
except DecidioHttpError as e:
if not e.retryable:
alert_oncall(str(e)) # a fresh token is needed; no amount of retrying helps
raise
backoff_and_retry()
Invariants
Decidio never executes downstream / holds no downstream credentials (the only downstream touch is the opt-in, read-only read-back tier) · holds none of the parked payload · fail-closed signatures · no automatic re-invocation once invocation may have begun · deny-by-default policy · request-bound identity proof. The agent executes; Decidio gates, records, and signals.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file decidio-0.2.1.tar.gz.
File metadata
- Download URL: decidio-0.2.1.tar.gz
- Upload date:
- Size: 124.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8e1f3e05c066f252c707ac4d7551d669e33b3e3214acbb3cfeb82877c82e708
|
|
| MD5 |
fd335d744745439122da4726aa4e5bb7
|
|
| BLAKE2b-256 |
455099e1607c912389a80c90cb3f2ef34a708b5dc0d5b2cc87184b5afcba23ce
|
File details
Details for the file decidio-0.2.1-py3-none-any.whl.
File metadata
- Download URL: decidio-0.2.1-py3-none-any.whl
- Upload date:
- Size: 137.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6afa3a6138cc86dd35834d595be1a5f6113f25df7ee5643df2708c8914495f6d
|
|
| MD5 |
8a999850912036306531bda644f9ea23
|
|
| BLAKE2b-256 |
19c41adad77d68d6c2d93795d14fd6d27182505239914c5b091a218795d3c33e
|