open-standards/openpay/Cargo.toml
Transaction Science 4275611a57
Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 5m12s
op-agentic: accept an agent's mandate, and refuse a narrowing we cannot evaluate
OpenPay could accept a machine payment inline on the `402` rail but had no way
to accept the other shape the agentic stack settled on: a buyer's agent
presents a mandate — a signed, selectively-disclosed credential stating what
its user authorized — and the merchant decides whether the checkout in front of
it falls inside that authorization. Its only agentic surface was the x402
crate; its "mandate" code was SEPA direct debit, unrelated.

The format is AP2's, implemented against the specification rather than a
summary of it: an SD-JWT whose `vct` names a family and a stage. A closed
mandate commits to one merchant-signed checkout by hash — the UCP Checkout
object, where the two are used together. An open mandate authorizes a *set* of
checkouts through constraints, and a closed one is chained from it.

Three places where the obvious implementation is the dangerous one:

**A withheld value is not an empty one.** Selective disclosure means the
payload carries digests and the holder reveals some. Resolution therefore
reports how many placeholders had no disclosure, because after an undisclosed
array element is removed — which the format requires — the array is
indistinguishable from one that was always empty. AP2's allowed-merchants rule
turns on exactly that difference: it is *invalid*, not permissive, when nothing
is revealed. A holder who withholds every merchant has authorized no merchant,
and that is asserted end to end, not only on the constraint type.

**Line items is a matching problem.** The specification says so and gives the
construction, so it is a maximal flow here: met only when the flow saturates
both the constraint's quantities and the checkout's. Both equalities carry
weight. Short of the constraint's total, the checkout lacks something the user
authorized; short of the checkout's total, it contains something the user did
not. A containment check misses the second, and a per-entry check lets one item
satisfy two entries. The specification's own worked example is a test case,
including every combination it lists as invalid.

**A constraint we do not evaluate is refused, not skipped.** Skipping is how an
agent exceeds its authority: the constraint exists because the user narrowed
something. The payment-mandate constraints — amount range, budget, agent
recurrence, allowed payee — are named as known and unimplemented, and refused.

Also enforced: the closed mandate's `checkout_hash` must cover the
`checkout_jwt` actually presented, under the presentation's own `_sd_alg`; an
unversioned `vct` is refused, since the suffix is how a reader knows which
rules apply; a mandate stating no `exp` is refused rather than read as valid
forever; and an unknown UCP capability requires a mandate, which is the safe
direction.

No signatures are verified here. The issuer signature and the holder's key
binding go through a `MandateVerifier` seam, the same swappable-driver edge the
card and x402 rails use — so the crate carries no cryptographic dependency
beyond the digest the format's own binding needs. What it does itself is the
part specific to the format and easy to get wrong.

The Checkout object is deliberately not modelled: its payload is expressly
outside the mandate specification's scope, so the caller supplies the checkout
facts it parsed rather than this crate inventing the one schema it was not
handed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-10 12:49:56 -05:00

156 lines
4.9 KiB
TOML

[workspace]
resolver = "3"
members = [
"crates/op-core",
"crates/op-iso20022",
"crates/op-iso8583",
"crates/op-emv",
"crates/op-vault",
"crates/op-fraud",
"crates/op-rails-card",
"crates/op-mobile-wallets",
"crates/op-rails-a2a",
"crates/op-ffi-swift",
"crates/op-ffi-jni",
"crates/op-wasm",
"crates/op-orchestrator",
"crates/op-ledger",
"crates/op-webhook",
"crates/op-graph",
"crates/op-reconciliation",
"crates/op-refund",
"crates/op-dispute",
"crates/op-dispute-evidence",
"crates/op-settlement",
"crates/op-batch",
"crates/op-server",
"crates/op-driver-sdk",
"crates/op-rails-crypto",
"crates/op-rails-x402",
"crates/op-agentic",
"crates/op-subscriptions",
"crates/op-fx",
"crates/op-routing",
"crates/op-screening",
"crates/op-bnpl",
"crates/op-3ds2",
"crates/op-cli",
"crates/op-connect",
"crates/op-tax",
"crates/op-revrec",
"crates/op-ewallets-asia",
"crates/op-openbanking",
"crates/op-payouts",
"crates/op-statements",
"crates/op-fraud-graph",
"crates/op-bin",
"crates/op-crypto-extra",
"examples/headless-server",
"examples/kiosk-linux",
"examples/demo-merchant",
"examples/pci-zero",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/Transaction-Science/open-standards"
authors = ["OpenPay Contributors"]
[workspace.dependencies]
# Core
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zeroize = { version = "1.8", features = ["derive"] }
subtle = "2.6" # constant-time comparison
# ISO 20022 — verified ground truth (Open-Payments/messages, Apache-2.0).
# Pin tightly: financial message layouts must not silently change under us.
# Main crate is at =1.0.10; the fednow companion lagged behind and
# stopped at =1.0.9 (the two crates version independently). The fednow
# surface we depend on is unchanged across 1.0.x, so the version skew
# is safe.
open-payments-iso20022 = { version = "=1.0.10", features = ["payments"] }
open-payments-fednow = "=1.0.9"
# XML codec — quick-xml is the de-facto choice; we use it for round-trip
# validation against XSD-canonical sample files.
quick-xml = { version = "0.41", features = ["serialize"] }
# ML / inference
burn = { version = "0.18", default-features = false }
ort = { version = "2", default-features = false }
# FFI
cxx = "1"
jni = "0.21"
wasm-bindgen = "0.2"
# Time, ids
# `serde` for OffsetDateTime serialization in domain structs;
# `formatting`/`parsing` for op-iso20022 dt formatters.
time = { version = "0.3", default-features = false, features = ["std", "serde", "formatting", "parsing", "macros"] }
# `v4` for random ids (events, attempts, idempotency tokens);
# `v7` for time-sortable ids in ledger/webhook bookkeeping;
# `v5` for deterministic (name-based) statement-line vertex ids so
# re-recording the same reconciliation line reuses its vertex.
uuid = { version = "1", features = ["v4", "v5", "v7", "serde"] }
# Testing
proptest = "1"
insta = "1"
# Observability — structured tracing. Operators wire their own
# `tracing-subscriber` to emit JSON, OpenTelemetry, or whatever
# their pipeline expects. `tracing` is the de-facto Rust standard;
# emits zero-cost when no subscriber is registered.
tracing = "0.1"
# HTTP server. `axum` 0.8 atop tokio is the de-facto choice in 2026;
# tower middleware lets operators add auth / logging / metrics
# layers without touching our handler code.
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "sync"] }
axum = "0.8"
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["trace"] }
http = "1"
http-body-util = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
# Live HTTP client. Used by ReqwestTransport (op-webhook fanout) and
# the JSON-RPC client in op-rails-crypto's EVM gateway. Blocking
# client keeps the sync trait surfaces intact.
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
# CLI parsing for op-cli.
clap = { version = "4", features = ["derive", "env"] }
# Hex encoding for EVM calldata + tx hashes.
hex = "0.4"
# Keccak-256 for EVM function selectors.
sha3 = "0.10"
# secp256k1 ECDSA via k256. Pure-Rust, audited, used by the
# LocalKeyEvmSigner so operators can launch with a hot-wallet
# signer before wiring KMS / Fireblocks. RFC 6979 deterministic
# nonces, recoverable signatures.
k256 = { version = "0.13", features = ["ecdsa", "std"] }
# RLP encoding for legacy / EIP-1559 EVM transactions. Used by
# LocalKeyEvmSigner to produce the raw-tx bytes broadcast by
# eth_sendRawTransaction.
rlp = "0.5"
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"
[profile.release-with-debug]
inherits = "release"
debug = true
strip = "none"