fix(security): harden retry, SSE, Redis and cost-budget paths

The gateway refuses a request for an unpriced model when the caller has a
cost budget. An unpriced model cost 0, so the budget counter was never
written and the budget could never fire. The new
allowUnpricedWithCostBudget setting restores the old behaviour.

The retry loop honours an upstream Retry-After of up to 30s and jitters
its own computed backoff. SSE parsing bounds the total size of one event.
The Redis client can use TLS. Provider base URLs and errors are scrubbed
of credentials before they reach logs or the admin view. The gateway
warns when failover and retry settings multiply into a large number of
outbound calls.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
2026-09-23 09:35:38 +01:00
parent 96b1a27ba5
commit b3b8ae6ad5
12 changed files with 576 additions and 50 deletions
+16
View File
@@ -76,6 +76,22 @@ type FailoverConfig struct {
const (
defaultFailoverMaxAttempts = 3
maxFailoverMaxAttempts = 10
// warnComposedOutboundCalls is the composed failover x retry worst
// case above which newGateway logs the product at construction.
//
// Security audit run-1: failover.maxAttempts and retry.attempts are
// each validated against their OWN ceiling and nothing validates their
// PRODUCT, yet they compose multiplicatively at runtime — the
// candidate loop in runMeteredCall wraps retryPolicy.do, so one
// admitted request can issue maxAttempts * (attempts+1) outbound
// provider calls, up to 10 * 4 = 40. Both features default off; with
// both merely switched on at their own defaults the product is
// 3 * 2 = 6. This threshold is that default-on product doubled, so an
// ordinary opt-in stays silent and only a deliberately wide
// configuration is surfaced — at config time, rather than during the
// upstream incident that makes it matter.
warnComposedOutboundCalls = 12
)
// failoverConfig is FailoverConfig, validated and defaulted once at