fix: surface extra.x402 UCP pay path (base-URI fallback + deepLinkCode) - #6
Open
Robin Schulte (relativvv) wants to merge 10 commits into
Open
fix: surface extra.x402 UCP pay path (base-URI fallback + deepLinkCode)#6Robin Schulte (relativvv) wants to merge 10 commits into
Robin Schulte (relativvv) wants to merge 10 commits into
Conversation
…402 is surfaced The augmenter read runtimeConfiguration.baseUri, which is compile-time-empty on this stack (StaticRuntimeConfigurationResolver), so extra.x402 (pay_url + deepLinkCode) was never attached to checkout.complete responses. Resolve the base URL from the Host header (carries host+port) via a pure UcpBaseUriResolver.
…USDC = 'USD Coin')
… param name The UCP checkout completion response exposed pay_url and deep_link_code as separate extra.x402 fields, requiring an agent to already know to append ?deepLinkCode=<code> itself. pay_url now ships with the code pre-assembled (deep_link_code stays as a sibling for back-compat); X402PayRequest already reads deepLinkCode from the query string first, so the pay route needs no change.
…ink null tests/bootstrap.php no longer requires the lane's vendor/autoload.php, which runs the lane's platform_check.php and aborts below PHP 8.4.1 - regressing the plugin's own PHP 8.2 floor and loading two autoloaders at once. Instead it captures the plugin's own ClassLoader and registers the vendored ucp-php-sdk's declared PSR-4 prefixes (Ucp\Sdk\ and Ucp\Sdk\Symfony\) directly, guarded by is_dir/is_file, mirroring the sibling SwagAgenticCommerce bootstrap's guard style. Also guards rawurlencode(getDeepLinkCode()) with ?? '' since the getter is nullable under strict_types.
… gate - bootstrap: probe multiple SDK locations, skip augmenter test when absent - settlement: keep class within cyclomatic budget via describeDomain helper - mago/depcheck: mirror src/Ucp ignores for the SDK-referencing Ucp test
The mainnet EIP-712 domain-name decoupling (assetEip712Name) is split out into its own standalone PR (#7) as the isolated mainnet blocker. This branch now carries only the base-URI resolver (§1), verify-failure logging + error meta (§1d), and deepLinkCode-in-pay_url (§1e), plus the test bootstrap / quality-gate machinery. Removed from here (now in #7): - X402RequirementBuilder: extra.name back to $config->assetSymbol - X402Config / X402ConfigService / config.xml: dropped assetEip712Name field - X402Fixtures / X402RequirementBuilderTest: dropped the assetEip712Name fixture param and the fallback/configured-name cases - docs/spec.md: dropped the EIP-712 domain-name note §1d coupling: X402SettlementServiceTest advertises the domain via the requirements' extra.name. With §1c gone, extra.name is assetSymbol again, so the test's fixture value and expected domain.name are set to 'USDC' (the fixture assetSymbol) instead of 'USD Coin'.
Defer the verify-failure diagnostics (X402Exception/X402SettlementService/ X402FacilitatorClient + services.xml + its test) and the standalone-CI test bootstrap (tests/bootstrap.php, phpunit.xml.dist, mago.toml, composer-dependency-analyser.php) to a separate follow-up PR. The augmenter unit test rides along with that bootstrap (it needs the SDK on the classpath), so it is deferred too. Net PR is now UcpBaseUriResolver (+test), the augmenter change, and its service wiring.
Author
|
Slimmed this PR down to its core: the UCP pay-path base-URI fallback ( Deferred to a follow-up PR:
Full suite green (90 tests). |
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.
This PR bundles the UCP pay-path correctness fixes and agent ergonomics improvements that surfaced while running the plugin end-to-end against a live shop (Base Sepolia settlement working; Base mainnet fix verified by unit test). Seven focused commits, grouped below.
Problem
extra.x402was never attached tocheckout.completeresponses. A UCP agent that completed a checkout had no discoverable way to pay the placed order — the augmenter silently produced nothing.invalid_payload. On Base mainnet, every x402 payment was rejected by the CDP facilitator withSWAG_X402__VERIFICATION_FAILED/invalid_payload; no funds moved. The identical code settled fine on Base Sepolia./verify, it returns onlyinvalidReason(invalid_payload) — a dead end for operator and agent alike.deepLinkCodewas undiscoverable. The pay route 403s (OWNERSHIP_PROOF_MISSING) until the deep-link code is supplied as?deepLinkCode=; the agent had to know to concatenatepay_urlanddeep_link_code.Root cause
baseUri.X402CheckoutResponseAugmenter::augment()built the pay URL from$context->runtimeConfiguration?->baseUri. With the SDK'sStaticRuntimeConfigurationResolverthat value is fixed at container-compile time and is empty when nobase_uriis configured, so the guardif (!$config->isComplete() || $baseUri === '')always took the empty branch and returned the checkout unchanged. (RequestContext::$hostis host-only, no port, so it is not a drop-in substitute for local/dev URLs.)extra.name/extra.version— the fields the buyer signs over and the facilitator uses to recover the signature.X402RequirementBuildersetextra.nametoassetSymbol(default"USDC"). But the EIP-712 domain name is not the symbol: Base mainnet USDC (0x8335…2913) has on-chainname() = "USD Coin"(version"2"), while Base Sepolia test USDC (0x036C…CF7e) really is named"USDC". So the shop advertised"USDC"on mainnet, the facilitator rebuilt the domain with the wrong name, signature recovery failed →invalid_payload. Testnet passed only because the test token's real name happens to be"USDC". (Bothname()s verified on-chain.)What changed
Surface the pay path (base-URI resolver):
src/Ucp/UcpBaseUriResolver.php(new) — a pure helper that resolves the base URL from the configuredbaseUriif present, else from the HTTPHostheader (which carries host+port), choosinghttpfor local hosts andhttpsotherwise.src/Ucp/X402CheckoutResponseAugmenter.php— inject and use the resolver instead of readingruntimeConfiguration->baseUridirectly.src/Resources/config/services_ucp.xml— register the resolver, inject it as the augmenter's third argument.tests/Unit/Ucp/UcpBaseUriResolverTest.php(new).Correct the mainnet EIP-712 domain name:
src/Core/X402/Config/X402Config.php+X402ConfigService.php+src/Resources/config/config.xml— new optional configassetEip712Name;assetSymbol's label clarified to "display only".src/Core/X402/X402RequirementBuilder.php—extra.nameis nowassetEip712Name !== '' ? assetEip712Name : assetSymbol(empty falls back to the symbol, so testnet is unchanged).tests/Unit/Core/X402/X402RequirementBuilderTest.php+tests/Unit/Support/X402Fixtures.php— fallback + configured-name cases.docs/spec.md— documents the EIP-712 domain-name requirement.Actionable verification errors:
src/Core/X402/X402FacilitatorClient.php— success log enriched withnetwork,asset,domainName,domainVersion.src/Core/X402/X402SettlementService.php— injectPsr\Log\LoggerInterface(registered inservices.xml); log awarningwith the advertised domain + network + asset + raw facilitator body before the verify-failure throw.src/Core/X402/Exception/X402Exception.php—verificationFailed()takes an optionalarray $domainand surfaces it undermeta.parameters.domain(name/version/network/asset) so an agent can self-diagnose a domain mismatch. Message template unchanged.tests/Unit/Core/X402/X402SettlementServiceTest.php— asserts the domain in the error params.Pre-assemble
deepLinkCodeintopay_url:src/Ucp/X402CheckoutResponseAugmenter.php—pay_urlnow ships with?deepLinkCode=<rawurlencode(code)>already appended;deep_link_codestays as a sibling for back-compat. (X402PayRequest::extractDeepLinkCodereads the query param first, so the pay route needs no change.) Null-safe understrict_types(getDeepLinkCode() ?? '').tests/bootstrap.php(new) +phpunit.xml.dist— the plugin does not declareucp-php-sdk(it is provided by the platform lane at runtime); the bootstrap loads the plugin autoloader then registers the SDK's PSR-4 prefixes (guarded) so unit tests can constructUcp\Sdk\*types without pulling in the lane's higher PHP floor.tests/Unit/Ucp/X402CheckoutResponseAugmenterTest.php(new).Testing
php -lclean on all changed PHP files.vendor/bin/phpunit→ 95 tests, 197 assertions; 93 passing, 2 errors. Both errors areX402CheckoutResponseAugmenterTestfailing withInterface "Ucp\Sdk\Contract\CheckoutResponseAugmenterInterface" not found— see the SDK note below; every other test (including the base-URI resolver, requirement builder, and settlement-service tests central to this PR) passes.complete_in_progress+x402→ on-chain USDC settlement → orderpaid. The mainnet EIP-712 fix was verified by unit test; a live mainnet purchase is the follow-up. Config-only unblock for an existing mainnet shop:system:config:set SwagX402Payments.config.assetEip712Name "USD Coin".Notes for reviewers
ucp-php-sdkto be resolvable. The bundledtests/bootstrap.phplocates it at a monorepo-relative path (../../../../vendor/ucp-php-sdk) that reflects the agent-shop platform-lane layout; in this standalone repo that path does not exist, so those two tests error withInterface "Ucp\Sdk\Contract\CheckoutResponseAugmenterInterface" not found. This is a pre-existing environment gap (upstreammainshipssrc/Ucp/*usingUcp\Sdk\*but has no UCP unit tests, andphpunit.xml.distbootstrapsvendor/autoload.phpwhich never provides the SDK). To run these two tests upstream, please decide how CI should provide the SDK (e.g.require-dev+ arepositoriesentry, or a CI checkout step) and generalize the bootstrap's SDK discovery. This is exactly the "align the bootstrap with the sibling plugin's mutually-exclusive lane detection" follow-up flagged in the source change notes.X402Config.php,X402ConfigService.php,config.xml, andservices.xml, which are also touched by the companion draft "feat: CDP mainnet facilitator auth (per-request signed EdDSA JWT)". That CDP PR should land first; this branch was replayed offmainwithout the CDP fields (only theassetEip712Namecorrectness field was kept in the config classes), so a straightforward rebase after the CDP PR merges will reconcile the two.extra.x402object is serialized as a top-levelx402key in the response (becauseUcp\Sdk\Model\Checkout\Checkout::toArray()doesarray_merge($data, $extra)), not nested underextra. Not changed here; documented so integrators look in the right place.