Skip to content

feat(solana): harden media surface + wire mid-poll re-sign payment guard#19

Merged
VickyXAI merged 1 commit into
mainfrom
fix/solana-media-hardening
Jul 4, 2026
Merged

feat(solana): harden media surface + wire mid-poll re-sign payment guard#19
VickyXAI merged 1 commit into
mainfrom
fix/solana-media-hardening

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

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

  • Re-price/redirect guard wired in. _assert_same_payment_terms was defined but never called (the orig_amount/orig_pay_to capture 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 raises PaymentError instead of authorizing an unbounded, unrelated payment. This PaymentError deliberately propagates — it is not swallowed by the fall-through that surfaces the original 402.
  • Re-sign robustness (sync + async). The challenge GET and signing are now guarded so a network/signing failure surfaces the gateway's real 402 reason instead of masking it. (The async challenge GET was previously unwrapped.)
  • URL-segment injection. _safe_path_segment validates every network/symbol/market/wallet value f-string'd into a paid endpoint path — these often come from LLM output.

Correctness / Base parity

  • list_voices returns the gateway's {"data":[...]} list, not the whole envelope dict.
  • price() uses data.get("price") so a paid body missing price raises a clean validation error, not a raw KeyError after the charge already 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 (15 tests): the payment-terms guard (pass / amount-change / recipient-change / type-coercion), media dispatch (music/speech/sfx body + endpoint), the list_voices envelope 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 carry pay_to.

286 passed, 15 skipped; ruff + black clean.

Known follow-ups (not in this PR)

  • The async re-sign doesn't yet run the re-price guard — it needs submit-time payload threading through _sign_payment_from_response (7 callers), which I didn't want to reshape here. Async is otherwise unchanged from Base.
  • validate_resource_url was imported but unused; I dropped it. Wiring poll_url redirect validation is a separate, deliberate change (it swaps in a "safe default" on host mismatch, which needs its own testing).

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 VickyXAI merged commit 32e54ec into main Jul 4, 2026
2 of 3 checks passed
@VickyXAI VickyXAI deleted the fix/solana-media-hardening branch July 4, 2026 05:57
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>
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