Skip to content

security: make money side effects atomic with their guards (ledger integrity)#94

Merged
tcconnally merged 1 commit into
mainfrom
fix/money-atomicity
Jul 5, 2026
Merged

security: make money side effects atomic with their guards (ledger integrity)#94
tcconnally merged 1 commit into
mainfrom
fix/money-atomicity

Conversation

@tcconnally

Copy link
Copy Markdown
Contributor

Ledger atomicity — HIGH #4/#5 + MED #6 + LOW #14 (2026-07-05 security review)

The most important PR for billing correctness. Shared root cause of #4/#5/#6: a
money side effect committed in a different transaction than the marker
guarding it, leaving a crash window.

  • CI workflow — lint + smoke test on push #5/README — add terminal recording (asciinema) and HTML dashboard screenshot #6 (HIGH/MED) Webhookmark_stripe_event committed the event claim
    separately from the credit add_ledger. A crash between them + Stripe's retry
    → event seen as "duplicate", credit never applied (silent credit loss);
    concurrent refund/dispute events for one charge could also double-reverse.
    handle_webhook_event now wraps the claim + all appliers in one db.immediate
    (BEGIN IMMEDIATE) transaction, commit=False threaded through
    mark_stripe_event/add_ledger/set_org_tier. Any error rolls back the whole
    thing (claim included) → clean retry; the manual unmark is no longer needed.
  • Tag v0.1.0 — freeze CLI surface, add --version flag #4 (HIGH) /v1/usage idempotency double-debit — the batch committed but the
    key's status flipped in a later separate commit; a crash in between left
    committed events behind a NULL-status claim that the 120s reclaim deleted,
    letting a retry re-record the batch. The response is now stored inside the same
    db.immediate block as the debits (extracted _usage_response helper).
  • Backtest mode — replay history against a policy #14 (LOW) Hard-stop — now decided in integer micro-dollars
    (get_balance_micros/usd_to_micros), not float USD.

Tests

263 pass (pytest tests/ -q), +5 new in test_txn_atomicity.py: webhook
apply-failure rolls back the claim + balance (then retry credits once), duplicate
event credits once, hard-stop micro boundary (drains to exactly 0 then blocks),
idempotent response stored with non-NULL status, retry replays without re-debit.
No behavior change to the existing 258 (webhook-reversal + money-cluster suites
still green).

…tegrity)

Closes three transaction-atomicity findings from the 2026-07-05 review whose
shared root cause was a money side effect committing in a different transaction
than the marker guarding it:

- Webhook (F2/F5/F6): claim + credit/tier/refund now commit in one db.immediate
  transaction (commit=False threaded through mark_stripe_event/add_ledger/
  set_org_tier); a crash between claim and credit no longer loses the credit, and
  concurrent refund/dispute events for one charge can't double-reverse. Rollback
  on error replaces the manual unmark.
- /v1/usage idempotency (#4): the response/status is stored inside the same
  db.immediate block as the debits, so a crash can't leave committed events behind
  a reclaimable NULL-status claim that a retry would re-record (double-debit).
- Hard-stop (#14): decided in integer micro-dollars, not float USD.

263 tests pass (+5: webhook apply-failure rollback, hard-stop micro boundary,
idempotent atomic-store + replay-no-double-debit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tcconnally tcconnally merged commit cc15045 into main Jul 5, 2026
8 checks passed
@tcconnally tcconnally deleted the fix/money-atomicity branch July 5, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant