Compare · API docs · Get access

Checks — a policy that decides, sealed like a human's

Not every approval needs a person. Put your own HTTP endpoint in front of an agent action: it decides approve or reject by your rules, in milliseconds — and Raposa seals that verdict in the exact same hash-chained audit trail a human decision gets. When the rule is unsure, it hands off to a named person.

validator-based approval · same audit chain · fail-closed · live since 2026-09-07

What it is

A validator is a URL you own. When your agent asks Raposa to approve an action, Raposa POSTs the request to your endpoint — signed with your webhook secret — and reads back {"decision":"approve|reject"}. Approve happens instantly under the actor validator:<host>; reject either escalates to a named human or rejects outright. Every step is written to the same tamper-evident audit chain as a person's decision, and the same signed webhook fires. Leave the validator out and nothing changes — the classic human approval works exactly as before.

Finance

Auto-approve refunds and payouts under a limit; anything above the line goes to a human — with their name on the decision.

Compliance

Encode the rule once as an endpoint: allowlists, jurisdiction checks, schema validation. Every verdict is provable in an export an auditor accepts.

Autonomous agents

A programmable gate in front of money-moving or irreversible actions, without building your own approval store, audit log or webhook retry.

How a verdict is resolved

Validator answersescalate_to setOutcome
approveapproved · actor validator:<host> · webhook fires
rejectnorejected · actor validator:<host> · webhook fires
rejectyesstays pending → a named human decides
error / unreachable / malformedyesstays pending → escalated to a human
error / unreachable / malformednostays pending — never auto-approved
Silence is not consent. A validator that times out, returns a 5xx, or sends back anything that is not a clear decision is treated as an error — and an error is never an approval. It escalates to a human if you named one, otherwise the request stays pending until it expires. This is enforced, not a convention.

With proof, not adjectives

Every line below is the docstring of a test that ran against this codebase today. If a test goes red, its line comes off this page before it is published.

ClaimBacked by
A validator's approve is sealed exactly like a human decision — status, hash-chain audit under actor validator:<host>, and the signed webhook.test_validator_approve_seals_like_a_human
A validator's reject, with no human named, rejects the request outright.test_validator_reject_auto_rejects
A reject with a named approver stays pending and hands off to that human.test_reject_with_escalation_hands_to_human
A validator error is never an approval — it escalates or stays pending.test_validator_error_is_never_an_approval
An approval created without a validator behaves byte-for-byte as before.test_no_validator_is_unchanged
A validator URL that resolves to a private address is refused at create time.test_private_validator_url_refused

And run live against production on 2026-09-07: an approve verdict resolved to approved with the audit trail approval_created → validator_attempt(http_200) → approval_decided; a reject resolved to rejected; a validator returning HTTP 500 left the request pending — the fail-closed path, in production, not on a slide.

Add a check in two calls

Your agent side — one call gates the action:

from raposa import guard   # pip install raposa

r = guard("refund $200 to order 4471",
          validator="https://your-app.example/raposa/validate",
          escalate="finance")          # reject or error -> a human decides
if r["status"] == "approved":
    do_refund()

Your policy side — the validator endpoint (verify the signature, then decide):

@app.post("/raposa/validate")
async def validate(request: Request, x_raposa_signature: str = Header("")):
    raw = await request.body()
    exp = "sha256=" + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(exp, x_raposa_signature):
        return {"decision": "reject", "comment": "bad signature"}
    amount = dollars((await request.json())["action"])
    if amount is not None and amount <= 1000:
        return {"decision": "approve", "comment": f"auto: ${amount} under limit"}
    return {"decision": "reject", "comment": "over auto-limit -> human"}

EU / GDPRhash-chained audit on every planHMAC-signed callsSSRF-guardedpriced per decision, not per approver