Silent-money bug (web/API path)
crossProviderPaymentAlias (internal/config/validation.go:151-153) coerces an Azure partial-upfront payment option to "upfront" before validation, with the comment "coerce to the all-upfront tier so the rec survives validation."
On the web/API purchase path this means:
POST purchase {provider: azure, service: compute, payment: "partial-upfront"}
-> normalized to "upfront" (internal/api/validation.go:568)
-> passes the {upfront, monthly} whitelist
-> rec.Payment = "upfront"
-> buildReservationBody maps to billingPlan=Upfront
-> purchased ALL-UPFRONT silently, on a schedule the caller did not request
Why it matters now
PR #1495 wired Azure billingPlan from the payment option. Before that, Azure always billed upfront regardless, so the coercion was invisible. Now the coerced value drives a live billing schedule, so the silent coercion produces a real cash-flow outcome the caller did not choose. It also contradicts the contract the MCP tool and docs advertise ("partial-upfront is a hard error rather than silently purchased under all-upfront or no-upfront").
This is the exact class the project's feedback_no_silent_fallbacks rule targets: a money-affecting field silently rewritten instead of rejected.
Mitigating factors (why should-fix, not blocker)
- The normal web UI only offers converter-generated recommendations (
providers/azure/internal/recommendations/converter.go:391,397 emit only upfront/monthly), so a typical user cannot trigger this; it requires a hand-crafted partial-upfront payload.
- Azure charges the same total for upfront vs monthly, so this is a billing-schedule/cash-flow mismatch, not an overcharge.
- Surface inconsistency: the CLI path now fails loud on Azure
partial-upfront (errors in buildReservationBody), while the web path coerces. The two surfaces should behave the same.
Fix
Reject Azure partial-upfront at the web/API validation boundary (fail loud, consistent with the CLI and MCP surfaces) instead of aliasing it to upfront. Azure supports only Upfront and Monthly; partial-upfront has no Azure equivalent and must not be silently mapped.
Found by
Adversarial money-path review during PR #1495 (Azure billing-plan wiring). Pre-existing (the alias predates #1495); #1495 is what makes the coercion reach a live billingPlan.
Silent-money bug (web/API path)
crossProviderPaymentAlias(internal/config/validation.go:151-153) coerces an Azurepartial-upfrontpayment option to"upfront"before validation, with the comment "coerce to the all-upfront tier so the rec survives validation."On the web/API purchase path this means:
Why it matters now
PR #1495 wired Azure
billingPlanfrom the payment option. Before that, Azure always billed upfront regardless, so the coercion was invisible. Now the coerced value drives a live billing schedule, so the silent coercion produces a real cash-flow outcome the caller did not choose. It also contradicts the contract the MCP tool and docs advertise ("partial-upfront is a hard error rather than silently purchased under all-upfront or no-upfront").This is the exact class the project's
feedback_no_silent_fallbacksrule targets: a money-affecting field silently rewritten instead of rejected.Mitigating factors (why should-fix, not blocker)
providers/azure/internal/recommendations/converter.go:391,397emit onlyupfront/monthly), so a typical user cannot trigger this; it requires a hand-craftedpartial-upfrontpayload.partial-upfront(errors inbuildReservationBody), while the web path coerces. The two surfaces should behave the same.Fix
Reject Azure
partial-upfrontat the web/API validation boundary (fail loud, consistent with the CLI and MCP surfaces) instead of aliasing it toupfront. Azure supports onlyUpfrontandMonthly;partial-upfronthas no Azure equivalent and must not be silently mapped.Found by
Adversarial money-path review during PR #1495 (Azure billing-plan wiring). Pre-existing (the alias predates #1495); #1495 is what makes the coercion reach a live billingPlan.