Skip to content

fix(l402): refuse unknown/unbounded invoice amount even with no max (#71) - #11

Merged
refined-element merged 1 commit into
masterfrom
fix/l402-refuse-unknown-amount-no-max
Jul 23, 2026
Merged

fix(l402): refuse unknown/unbounded invoice amount even with no max (#71)#11
refined-element merged 1 commit into
masterfrom
fix/l402-refuse-unknown-amount-no-max

Conversation

@refined-element

Copy link
Copy Markdown
Owner

Ledger #71 — fail-closed the L402 amount/budget gate

The L402 amount/budget gate short-circuited when no max_amount_sats was configured (if effective_max is None: return) and paid anything — an unbounded, unaudited spend whenever a caller merely forgot to set a ceiling.

Change

L402Client._check_amount_against_max is split into two independent rules:

  1. Unknown/unbounded amount → ALWAYS REFUSE, whether or not a max is set. "Unknown/unbounded" = amountless invoice, unparseable, or <= 0 (all reported as None by _decode_invoice_amount_sats). Fail-closed core of #71.
  2. Ceiling comparison (amount > max → refuse) applies only when a max is configured.

Result: unknown amount → refuse (even with no max); known positive amount + no max → pay (caller's opt-out); known positive amount + over max → refuse. It does not force every payment to declare a max.

Tests (failing-test-first)

  • Flipped test_unparseable_invoice_still_paid_when_no_budget_configuredtest_unparseable_invoice_refused_even_when_no_budget_configured (was RED against pre-fix code: it paid; now GREEN). Mutation-checked: reverting the fix makes it pay again.
  • Added test_known_amount_paid_when_no_budget_configured pinning the opt-out half.
  • Existing "max set → refuse over-max / refuse unknown" tests unchanged and passing.

Full suite: 211 → 212 passing (net +1 test).

Not published, no version bump.

…#71)

The L402 amount/budget gate short-circuited when no `max_amount_sats` was
configured (`if effective_max is None: return`) and paid ANY invoice — an
unbounded, unaudited spend whenever a caller merely forgot to set a ceiling.

Split the gate into two independent rules:

1. Unknown/unbounded amount (amountless, unparseable, or <= 0 — all reported
   as None by the decoder) is ALWAYS refused, whether or not a max is set.
   This is the fail-closed core of ledger #71.
2. The `amount > max` ceiling comparison applies ONLY when a max is
   configured. A KNOWN positive amount with no ceiling is still paid — the
   caller's documented opt-out — so this does not force every payment to
   declare a max.

Flips the security-regression test that pinned the fail-open pay behavior to
assert refusal, and adds a companion test proving a known amount with no
ceiling is still paid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@refined-element
refined-element merged commit 22b380b into master Jul 23, 2026
6 checks passed
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