jcp-x402: refuse a payment scheme this crate does not implement #442
Merged
dcharlot
merged 1 commit from 2026-10-22 12:04:49 -04:00
fix/x402-refuse-unimplemented-scheme into main 2026-10-22
jcp-x402: refuse a payment scheme this crate does not implement
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 42s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m38s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m9s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m10s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, -p sandbox-tee --features amd-kds,intel-pcs, sandbox-rs, linux) (pull_request) Successful in 2m40s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m12s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m55s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 59s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m50s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 3m2s
`ingest_requirements` parsed `scheme`, defaulted an empty value to `"exact"`, and then **nothing ever read the field** — `git grep '\.scheme\b' -- jcp/` returns zero hits. So a requirement naming any other scheme was gated exactly as if it were `exact`. That matters most for x402's `upto`, which means "charge up to `maxAmountRequired`, settle for the actual amount". Accepted as `exact`, its per-request **maximum** is debited as the price: `spent_minor` accumulates `price.minor` at authorization (`jcp-runtime` lib.rs ~315 and ~931), and `seal_settlement` passes `Price::new(&req.asset, 0)` — its own comment says "debits nothing, reads nothing". Nothing ever reconciles authorized down to settled, so a session settling at a tenth of its cap exhausts a money ceiling ten times too fast. The fix is to refuse, not to reconcile. This crate implements `exact`, where the authorized price *is* the settled price — so `spent_minor` accumulating `price.minor` at authorization is exact by construction and there is nothing to reconcile. Honouring `upto` would need a settle-down debit that does not exist, and inventing one would mean making `spent_minor` non-monotonic, which is a change to the ceiling's soundness model rather than a bug fix. Supporting `upto` is a feature; proceeding under semantics the code does not implement is the defect. `X402Error::UnsupportedScheme` is a new variant rather than `Malformed`, because the document is well formed — this crate simply does not honour what it asks for. An absent scheme stays `exact`, which is the v1 default and the semantics the money path implements; an existing test that omits the field proved that is load-bearing when a mutation removed it. The check is an allowlist, not a denylist of `upto`, and is ordered **before any figure is parsed**. A caller that fixed a broken amount would otherwise be led to retry into semantics this code does not implement. That ordering was wrong in my first version and a test caught it: I wrote "settled before any figure below is read" in the comment while placing the check after `maxAmountRequired` was parsed, so the document reported a malformed amount instead of the unsupported scheme. The claim was false until the check moved above every figure. Falsification, three mutations, all killed by their named test: the pre-fix behaviour (any scheme accepted as exact); refusing an absent scheme too (over-strict — also breaks an existing test that omits it); and a denylist of one name instead of an allowlist. Found by the 2026-10-22 landscape sweep, which ranked it against EMVCo's agentic-payments framework draft. The EMVCo draft itself is log-in gated and its licence excludes derivatives, so it was not retrieved or used; the shipped counterpart x402 (Apache-2.0, spec v2) is what this reads. Verified: workspace check 0 errors, 66 test binaries green, 0 failures, clippy `-D warnings` clean, `check-spec-refs` 1773 citations across 16 standards, 47 lockfiles current. Co-Authored-By: Claude Opus 5 <[email protected]>