feat(solana): harden media surface + wire mid-poll re-sign payment guard#19
Merged
Conversation
Follow-up hardening on the Solana media methods added in #16, addressing a two-model review (correctness/money-path + parity) and completing the in-progress security work. Security / money-path: - Wire _assert_same_payment_terms into the sync mid-poll re-sign: a fresh 402 challenge that reprices or redirects the payment vs. what the job originally authorized now raises PaymentError instead of signing an unbounded, unrelated payment. The guard was defined but never called (dead orig_amount/orig_pay_to capture); now enforced and unit-tested. - Sync + async re-sign: guard the challenge GET and signing so a network or signing error surfaces the gateway's real 402 reason instead of masking it (async challenge GET was unwrapped before). - _safe_path_segment on every network/symbol/market/wallet URL segment (LLM-controlled values can no longer escape the path). Correctness / parity vs the Base clients: - list_voices returns the gateway's {"data":[...]} list, not the whole envelope dict. - price(): data.get("price") so a paid body missing "price" surfaces a clean validation error, not a raw KeyError after the charge settled. - RealFace group_id validated with the shared _GROUP_ID_RE (was truthy-only). - RPC/music/speech settlement receipt + gateway metadata plumbed via _attach_receipt / _last_raw_headers / _rpc_response. - MEDIA_POLL_MAX_RESIGNS 3 -> 2 to match Base VideoClient. Tests: - New tests/unit/test_solana_media.py: the payment-terms guard, media dispatch (music/speech/sound-effects body + endpoint), list_voices envelope, local validation (lyrics+instrumental, video exclusivity, face-id prefix, portrait url), price KeyError-safety, and path-segment injection rejection. - Fixed the timeout-test payment fake to carry pay_to (the guard reads it). Known follow-up: the async re-sign does not yet run the re-price guard (needs submit-time payload threading through _sign_payment_from_response); async is otherwise unchanged from Base. validate_resource_url import was dropped as unused — wiring poll_url redirect validation is a separate change. 286 passed, 15 skipped; ruff + black clean.
VickyXAI
pushed a commit
that referenced
this pull request
Jul 5, 2026
…>=3.10) test_solana_media.py (added in #19) had no version guard and isn't in CI's 3.9 ignore list, so on 3.9 — where x402[svm] isn't installed — the autouse codec-stub fixture's monkeypatch.setattr(...decode_payment_required_header) raised AttributeError and errored the whole module. This is why main went red on 3.9 after #19. Add pytest.importorskip("x402"/"solders"), matching test_solana_timeout_routing.py.
VickyXAI
added a commit
that referenced
this pull request
Jul 5, 2026
* fix(solana): async re-sign payment-terms guard + poll_url host pinning Closes the two follow-ups flagged in #19: - Async mid-poll re-sign now runs the same _assert_same_payment_terms guard as the sync path. The async media helper inlines the submit-time signing so it can capture the original amount/pay_to, and the re-sign block mirrors the sync structure (guard runs OUTSIDE the try/except so a re-price PaymentError propagates instead of being masked as a generic 402). - _absolute_url now host+scheme-pins an absolute poll_url to the API origin (sync + async). The poll loop sends and re-signs the wallet PAYMENT-SIGNATURE against poll_url, so a gateway response redirecting it off-host would leak the signed payment; reject it. Tests (tests/unit/test_solana_media.py): end-to-end re-sign through the poll loop for sync AND async (same-terms completes; re-price propagates), plus poll_url host-pin cases (relative resolved, same-host ok, cross-host and http-downgrade rejected). * test(solana): skip test_solana_media on Python 3.9 (x402 extras need >=3.10) test_solana_media.py (added in #19) had no version guard and isn't in CI's 3.9 ignore list, so on 3.9 — where x402[svm] isn't installed — the autouse codec-stub fixture's monkeypatch.setattr(...decode_payment_required_header) raised AttributeError and errored the whole module. This is why main went red on 3.9 after #19. Add pytest.importorskip("x402"/"solders"), matching test_solana_timeout_routing.py. --------- Co-authored-by: 1bcMax <viewitter@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up hardening on the Solana media methods added in #16, from a two-model review (correctness/money-path + Base parity) plus completion of the in-progress payment-terms guard.
Security / money-path
_assert_same_payment_termswas defined but never called (theorig_amount/orig_pay_tocapture was dead, and it crashed the image test fakes). It's now enforced on every sync mid-poll re-sign: a fresh 402 challenge that changes the amount or recipient raisesPaymentErrorinstead of authorizing an unbounded, unrelated payment. ThisPaymentErrordeliberately propagates — it is not swallowed by the fall-through that surfaces the original 402._safe_path_segmentvalidates everynetwork/symbol/market/walletvalue f-string'd into a paid endpoint path — these often come from LLM output.Correctness / Base parity
list_voicesreturns the gateway's{"data":[...]}list, not the whole envelope dict.price()usesdata.get("price")so a paid body missingpriceraises a clean validation error, not a rawKeyErrorafter the charge already settled.group_idvalidated with the shared_GROUP_ID_RE(was truthy-only)._attach_receipt/_last_raw_headers/_rpc_response.MEDIA_POLL_MAX_RESIGNS3 → 2 to match BaseVideoClient.Tests
New
tests/unit/test_solana_media.py(15 tests): the payment-terms guard (pass / amount-change / recipient-change / type-coercion), media dispatch (music/speech/sfx body + endpoint), thelist_voicesenvelope regression, local validation (lyrics+instrumental, video mutual-exclusivity, face-id prefix, portrait URL),price()KeyError-safety, and path-segment injection rejection. Also fixed the timeout-test payment fake to carrypay_to.286 passed, 15 skipped; ruff + black clean.
Known follow-ups (not in this PR)
_sign_payment_from_response(7 callers), which I didn't want to reshape here. Async is otherwise unchanged from Base.validate_resource_urlwas imported but unused; I dropped it. Wiringpoll_urlredirect validation is a separate, deliberate change (it swaps in a "safe default" on host mismatch, which needs its own testing).