jcp-x402: refuse a payment scheme this crate does not implement #442

Merged
dcharlot merged 1 commit from fix/x402-refuse-unimplemented-scheme into main 2026-10-22 12:04:49 -04:00
Owner

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.

Why that is a live money bug, not a tidy-up

x402's upto 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);
  • seal_settlement passes Price::new(&req.asset, 0) — its own comment reads "records the coupling; debits nothing, reads nothing."

Nothing reconciles authorized down to settled. 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 means making spent_minor non-monotonic — a change to the ceiling's soundness model, not a bug fix. Supporting upto is a feature; proceeding under semantics the code does not implement is the defect. Refusing closes the defect completely and leaves the feature properly scoped.

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: that is the v1 default and the semantics the money path implements. A mutation that refused absent schemes too broke an existing test which omits the field, so that default is load-bearing rather than decorative.

The check is an allowlist, not a denylist of upto, and is ordered before any figure is parsed — a caller who fixed a broken amount would otherwise be led to retry into semantics this code does not implement.

A test caught my own false claim

My first version wrote "settled before any figure below is read" in the comment while placing the check after maxAmountRequired was parsed. The document then reported a malformed amount instead of the unsupported scheme, and the_scheme_is_refused_before_the_amount_is_parsed failed. The comment was false until the check moved above every figure — the same defect class this PR fixes, in my own prose.

Falsification

mutation result
X1 any scheme silently accepted as exact (the pre-fix behaviour) an_unimplemented_scheme_is_refused_rather_than_treated_as_exact + 1 FAILED
X2 an absent scheme refused too (over-strict) an_absent_scheme_is_the_v1_exact_default + 1 FAILED (an existing test too)
X3 a denylist of one name instead of an allowlist only an_unimplemented_scheme_is_refused_rather_than_treated_as_exact FAILED

Provenance of the finding

Surfaced by the 2026-10-22 landscape sweep, ranked against EMVCo's Agentic Payments — Framework for Specifications v1.0 DRAFT. The EMVCo draft was not retrieved or used: it is log-in gated and its reproduction licence excludes derivatives. What this reads is the shipped counterpart, x402 (x402-foundation/x402, Apache-2.0, spec v2).

Verification

  • cargo check --workspace --all-targets --locked (jcp-rs) — 0 errors
  • cargo test --workspace --locked — 66 test binaries green, 0 failures
  • cargo clippy -p jcp-x402 --all-targets --no-deps --locked -- -D warnings — 0 findings
  • check-spec-refs 1773 citations / 16 standards · check-lockfiles 47 current

🤖 Generated with Claude Code

`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`. ## Why that is a live money bug, not a tidy-up x402's `upto` 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); - `seal_settlement` passes `Price::new(&req.asset, 0)` — its own comment reads *"records the coupling; debits nothing, reads nothing."* Nothing reconciles authorized down to settled. 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 means making `spent_minor` non-monotonic — a change to the ceiling's soundness model, not a bug fix. **Supporting `upto` is a feature; proceeding under semantics the code does not implement is the defect.** Refusing closes the defect completely and leaves the feature properly scoped. `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`: that is the v1 default and the semantics the money path implements. A mutation that refused absent schemes too broke an existing test which omits the field, so that default is load-bearing rather than decorative. The check is an **allowlist**, not a denylist of `upto`, and is ordered **before any figure is parsed** — a caller who fixed a broken amount would otherwise be led to retry into semantics this code does not implement. ## A test caught my own false claim My first version wrote *"settled before any figure below is read"* in the comment while placing the check **after** `maxAmountRequired` was parsed. The document then reported a malformed amount instead of the unsupported scheme, and `the_scheme_is_refused_before_the_amount_is_parsed` failed. The comment was false until the check moved above every figure — the same defect class this PR fixes, in my own prose. ## Falsification | mutation | result | |---|---| | **X1** any scheme silently accepted as exact (the pre-fix behaviour) | `an_unimplemented_scheme_is_refused_rather_than_treated_as_exact` + 1 FAILED | | **X2** an absent scheme refused too (over-strict) | `an_absent_scheme_is_the_v1_exact_default` + 1 FAILED (an existing test too) | | **X3** a denylist of one name instead of an allowlist | **only** `an_unimplemented_scheme_is_refused_rather_than_treated_as_exact` FAILED | ## Provenance of the finding Surfaced by the 2026-10-22 landscape sweep, ranked against EMVCo's *Agentic Payments — Framework for Specifications* v1.0 DRAFT. **The EMVCo draft was not retrieved or used**: it is log-in gated and its reproduction licence excludes derivatives. What this reads is the shipped counterpart, x402 (`x402-foundation/x402`, Apache-2.0, spec v2). ## Verification - `cargo check --workspace --all-targets --locked` (`jcp-rs`) — **0** errors - `cargo test --workspace --locked` — **66** test binaries green, **0** failures - `cargo clippy -p jcp-x402 --all-targets --no-deps --locked -- -D warnings` — **0** findings - `check-spec-refs` 1773 citations / 16 standards · `check-lockfiles` 47 current 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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
3005307df1
`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]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!442
No description provided.