github.audit_pull_requestabsorbed 3 faults · 4 attempts
347msTriadr runs one business workflow across three external applications - auditing a pull request on GitHub, collecting team sign-off in Telegram, releasing an escrow payout through Stripe. Every side effect passes through a gate that retries, reroutes, deduplicates and, when a step is genuinely unrecoverable, rolls the whole workflow back.
Audit PR #42 in mrnetwork/triadr, get team sign-off in #eng-approvals, then release $2,500.00 USD from escrow to acct_1TriadrContractorOne sentence in. Six gate-supervised steps out.
Real decisions from a chaos run and a rollback run · hover to pause
Across 40 runs at a ~75% first-attempt failure rate:
reproduce · python3 scripts/campaign.py --runs 40
The problem
Most multi-app agents are scripts. They work in a demo and break in production, because the failure they are least prepared for is the ordinary one: a 429, a 502, a vendor quietly renaming a field.
Stripe rate-limits the payout. The script raises and dies. But the GitHub commit status is already green, and the Telegram approval card is still sitting in the chat asking for a decision on a payment that will never happen.
The rate limit is classified, backed off and rerouted to a healthy gateway. If it genuinely cannot be recovered, the approval card is retracted and the commit status reset - in reverse order - before the run reports failure.
The thesis is small and testable: a multi-step workflow should end fully applied or fully reverted, and the record of which should be independently verifiable. Everything in Triadr exists to make that true.
Three connected apps
Every tool publishes a real JSON Schema contract. Every tool that mutates remote state names the tool that reverses it - a structural test enforces this, so the saga guarantee cannot rot as tools are added.
Code audit
Reads the pull request, its diff and its CI check runs, then scores the change on diff surface, sensitive paths, test coverage and build state - and writes the verdict back onto the commit.
github.get_pull_requestreadgithub.list_changed_filesreadgithub.get_check_runsreadgithub.audit_pull_requestreadgithub.set_commit_statuswritegithub.clear_statusundoRuns against the real GitHub API once GITHUB_TOKEN is set; simulated until then, and it says which.
Team approval
Posts an approval card carrying the audit verdict and the proposed payout with real inline Approve / Reject buttons, then long-polls until a human presses one - no public callback URL needed.
telegram.post_approval_cardwritetelegram.await_approvalreadtelegram.post_messagewritetelegram.delete_messageundoRuns against the real Telegram API once TELEGRAM_BOT_TOKEN is set; simulated until then, and it says which.
Escrow payout
Releases the contractor escrow as a Transfer under an idempotency key derived from the workload - so a retry whose response was lost in flight can never pay twice.
stripe.get_balancereadstripe.release_escrowpaymentstripe.get_transferreadstripe.reverse_transferundoRuns against the real Stripe API once STRIPE_SECRET_KEY is set; simulated until then, and it says which.
The plan shape is fixed in code. An LLM may extract parameters - repo, amount, chat - but can never add, remove or reorder a step. A hallucinated second release_escrow would be a financial incident, so the model is not given that authority.
The reliability gate
Every call to every app goes through one function. It runs in this order, and the first four all happen before a single packet leaves the process.
Keys derive from the workload, not the attempt, so a re-run of the same payout collapses onto one key. A replay returns the recorded result instead of moving money again.
A compiled subset of JSON Schema - no LLM, no third-party validator on the hot path. A malformed payload is rejected before the executor runs, so a bad call has no side effect at all.
Client-side rate shaping sized below each vendor's documented ceiling. Cheaper to shed load yourself than to absorb a 429.
Per endpoint, CLOSED → OPEN → HALF_OPEN. An open endpoint is never selected and costs no retry budget. If the whole fleet is open, one probe is still admitted - a breaker sheds load, it does not guarantee failure.
Rate limits, timeouts, partitions and auth expiry back off exponentially with full jitter and honour Retry-After. A 403 or 404 is terminal and never retried, because retrying it is pure latency.
Attempts cycle across health-ranked MCP gateways. Scores decay multiplicatively on failure and recover additively on success, so a flapping host is deprioritised without being banned.
A declared outputSchema catches drift on a tool’s very first call; a learned fingerprint catches it thereafter. A vendor renaming decision to decision_v2 is caught and rerouted, not silently fed into a payout decision.
If a critical step is unrecoverable, completed side effects are undone in reverse order - with a larger retry budget than the forward path, because the rollback is the one thing that must not fail.
Four scenarios
A reliability engine that only demonstrates its happy path has demonstrated nothing. Each scenario is one command, and the rollback path is injected at the app boundary so every layer above it behaves exactly as it would during a real outage.
The happy path: six steps, three apps, one settled payout.
python3 main.py --scenario cleanRoughly three in four calls fail on first attempt. The gate heals them; the payout still settles exactly once.
python3 main.py --scenario chaosStripe goes hard-down after the approval card is posted. Triadr retracts the card and resets the commit status - no money moved, no stale approval left in the channel.
python3 main.py --scenario rollbackThe team says no. The payout is skipped by an explicit condition, not by accident.
python3 main.py --scenario rejectedEvidence
Every figure on this page is produced by a script in the repo. Re-run it and the numbers regenerate; change the engine and they change with it.
Faults are injected on a seeded RNG, so the same seed replays the identical failure sequence and the identical recovery path.
The 14 rolled-back runs are the important column. At this fault rate a linear script would have left partial state in most of them - a posted approval card with no payout, or a payment with no record. Triadr left none.
python3 scripts/campaign.py --runs 40Both phases are reported, because quoting only the first would overstate the engine.
| Phase | p50 | p95 | p99 |
|---|---|---|---|
| Input schema validationGates every side effect before it happens | 1.42 µs | 1.50 µs | 1.71 µs |
| Full pre-flightThe above plus the response drift fingerprint | 4.92 µs | 5.25 µs | 6.54 µs |
python3 main.py --benchSchema edge cases (including True not satisfying integer), breaker state transitions, backoff bounds, endpoint deprioritisation, idempotency, drift detection, chaos determinism, the MCP JSON-RPC surface, LIVE request wiring for all fourteen tools, hash-chain tamper evidence - and an eight-seed property test asserting the core invariant.
python3 -m pytest tests/ -qCryptographic audit log
Every gate decision, injected fault, recovery and compensation becomes one entry in an append-only SHA-256 hash chain.
digest(n) = SHA256( digest(n−1) ‖ canonical_json(entry(n)) )Editing, reordering or deleting any entry breaks verification at exactly the corrupted index - demonstrated by test, not merely claimed.
Canonical serialisation fixes key order and separators, so the same entry hashes identically anywhere.
The attestation commits to every entry, and with TRIADR_LOG_KEY set it carries an HMAC-SHA256 signature. run.end is recorded before the seal, so the commitment covers the complete log.
python3 main.py --verify .triadr/<run_id>.jsonlThis is what separates a reliability number from a marketing number. The log is written to disk on every run, and the verifier is a standalone command that takes nothing on trust from the process that produced it.
Safety
| Property | Mechanism |
|---|---|
| No accidental live payments | Stripe refuses an sk_live_ key unless TRIADR_ALLOW_LIVE_MONEY=1 is set explicitly |
| No agent-invented payments | The plan shape is fixed in code; an LLM may only extract parameters, never add a step |
| No double payment | Workload-derived idempotency keys plus a replay ledger |
| No code execution from instructions | Condition evaluation is a small hand-written parser, never eval |
| No silent contract drift | Declared output schemas plus a learned per-tool response fingerprint |
| No unverifiable claims | Hash-chained log with a standalone verification command |
| Demo never mistaken for production | Every app reports LIVE or SIMULATED in results, dashboard and audit log |
Judges will find these anyway, so they are stated up front rather than buried.
Run it
Every app falls back to a deterministic simulator and says so, so the full demo runs offline. Adding a token switches that app to LIVE with no code change.
# stdlib only - no pip install neededpython3 main.py --scenario all # the test suite and the reliability campaignpython3 -m pytest tests/ -qpython3 scripts/campaign.py --runs 40python3 -m venv .venv.venv/bin/pip install -r requirements.txt # terminal 1 - control plane.venv/bin/uvicorn server:app --port 8000 # terminal 2 - dashboardnpm install && npm run devAll 14 tools are exposed over the standard MCP stdio transport, dependency-free. Every call an MCP host makes is gate-supervised, so the host inherits retry, rerouting, idempotency and rollback for free.
claude mcp add triadr -- python3 /path/to/Triadr/mcp_servers/stdio_server.pyThe dashboard streams every gate decision live - retries, reroutes, circuit breakers, rollbacks and the hash chain sealing itself as the run completes.
Pick a scenario, run the agent, and follow every retry, reroute and rollback as it streams - then verify the hash chain yourself.