The Passage faucet was not unique. The same shape — a client-sendable message that rewinds the guarding state — sat in both siblings, and for the same reason: nothing drove these handlers over a real connection. `ritual_start` resets `ritual_completed`, and the UI offers that button (honest play needs it after a failed roll). Every re-walk re-credited RITUAL_SUCCESS_ESSENCE and re-rolled an item drop. `ritual_step` has no limiter at all. Judgment was worse. Only `cross_over` was guarded; replaying any other verdict paid CORRECT_JUDGMENT_ESSENCE plus favor plus an item roll on every frame, unbounded. Favor is the damaging half — it pins at the +1.0 ceiling and then biases every future mint through apply_favor_bias, so the exploit permanently changed which spirits the seeker can meet. The frame also echoed the nominal deltas, which seance.tsx sums into displayed totals, so the screen and the ledger diverged. Guards mirror `passage_paid`: cleared only by a genuine summon, never by the rewinding message. `judged_verdicts` is keyed per verdict rather than a blanket latch, so correcting a wrong call still resolves; `test` is exempt since it never touches the ledger. Both frames now report what was applied. tests/test_ws_reward_replay.py drives each exploit and reads the ledger from the database. Proven both directions: reverting the guard fails with "minted 120 extra essence"; over-broadening it fails with "a fresh presence did not re-open the purse". Audited and found safe, with reasons in the report: summon trickle, device telemetry ingestion, at_peace writes, purchase_unlock, sigils, waitlist, scry/question/anomaly/manifest/fragment. Every essence write is with_for_update-locked. Known residual: guards live on SeanceState, so a reconnect resets them — but each payout still needs a summon, and summon_limiter is per user across connections, so income stays rate-bounded. Not closed, deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 KiB
60 KiB