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.
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.
Auto-approve refunds and payouts under a limit; anything above the line goes to a human — with their name on the decision.
Encode the rule once as an endpoint: allowlists, jurisdiction checks, schema validation. Every verdict is provable in an export an auditor accepts.
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 answers | escalate_to set | Outcome |
|---|---|---|
| approve | — | approved · actor validator:<host> · webhook fires |
| reject | no | rejected · actor validator:<host> · webhook fires |
| reject | yes | stays pending → a named human decides |
| error / unreachable / malformed | yes | stays pending → escalated to a human |
| error / unreachable / malformed | no | stays pending — never auto-approved |
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.
| Claim | Backed 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
raposa