Releases: runcycles/cycles-ap2-python
Releases · runcycles/cycles-ap2-python
v0.3.0
Changed (wire shape)
- AP2 routing context (
host,currency,payment_protocol) moved fromAction.policy_keystoSubject.dimensions["payee_website" / "payment_currency" / "payment_protocol"]. The v0.1/0.2 placement onAction.policy_keyswas per thecycles-action-kinds-v0.1.26.yamlextension; productioncycles-serverv0.1.25.x doesn't yet implement that extension and rejected the field with400 Malformed request body. v0.3 ships these values onSubject.dimensions(part of the base protocol), so the wrapper works against current production servers. The client-sideRuntimeAuthorityReceipt.policy_keysstill carries the canonical shape — dashboards / dispute evidence / audit pipelines that consumed the receipt are unchanged.
Unchanged
- Public Python API (
cycles_guard_payment,cycles_guard_payment_async, all exception classes, all class attributes,AP2Mandate,RuntimeAuthorityReceipt). - Exception contract.
- Idempotency-key derivation (still
ap2:open_mandate:{...}orap2:tx:{...}). - All v0.2.0 features (async, commit-uncertainty handling, cancellation handling).
Still planned for v0.4+
- Multi-currency (still raises
AP2CurrencyErroron non-USD). payment.refundconvenience helper.- Opt-in flag to re-emit
Action.policy_keysfor servers that implement the v0.1.26 extension. - Server-verifiable runtime-authority receipt (requires
cycles-protocolsigned-receipt field).
v0.2.0
Added
AsyncGuardedPaymentandcycles_guard_payment_async(...)— async-context-manager variant for asyncio runtimes (FastAPI, anyio, OpenAI async SDK, etc.). Same exception classes as sync plus one async-only condition (cancellation mid-commit, see below). Same idempotency-key derivation (including the open-mandate consume-once scope) and same commit-uncertainty handling as the syncGuardedPayment.- New example
examples/ap2_human_not_present_async.py. - README "Async variant (v0.2+)" quickstart snippet.
- 37 tests in
tests/test_async_guard.pymirroring the sync test surface (clean commit, dry-run, denial, release on exception, commit-uncertain branches incl. cancellation, commit-failed branches incl. release-failure recording, AP2 sample-type adapter end-to-end). Total 147 tests, 99.20% coverage. ruff + mypy strict.
Changed (async-only)
AP2GuardCommitUncertaingains one newerror_codevalue,"COMMIT_CANCELLED", raised when anasyncio.CancelledErrorlands while the async commit POST is in flight. The exception class itself is unchanged; only the set oferror_codediscriminators grew. Sync callers see no change.
Unchanged
- No wire-shape changes. No validation changes. No sync API changes — existing v0.1.x sync callers see the API entirely unchanged.
Still planned for v0.3
- Multi-currency.
payment.refundhelper.- Server-verifiable runtime-authority receipt (requires
cycles-protocolsigned-receipt field).
v0.1.0
Added
cycles_guard_payment(...)sync context manager:reserveon enter,commiton clean exit,releaseon exception.AP2Mandateadapter type insulating the wrapper from upstream AP2 schema churn.RuntimeAuthorityReceipt(runtime_authority.ap2.payment.charge.v1) — client-side, informational.- Deterministic idempotency keys with automatic consume-once scope selection —
ap2:open_mandate:{sha256(open_mandate_hash)[:32]}:{phase}when the mandate carries an open mandate hash (AP2 spec §6 normative consume-once defense for human-not-present flows),ap2:tx:{sha256(transaction_id)[:32]}:{phase}otherwise. Hash is fixed-length (128-bit collision resistance), header-safe, and the phase suffix is always preserved. AP2DryRunResultexception — raised from__enter__whendry_run=Trueso thewithbody cannot execute (prevents real PSP calls under dry-run from moving money off the books).AP2GuardCommitFailedexception — raised after a release attempt when the server rejects a commit with an unrecognized code; carriesreleased: boolandrelease_error: str | Noneso the caller can distinguish "budget recovered" from "budget stranded until TTL".AP2GuardCommitUncertainexception — raised whenever the commit outcome is unknown after the PSP body ran: terminal codes (RESERVATION_FINALIZED/RESERVATION_EXPIRED/IDEMPOTENCY_MISMATCH), transport-level failures (error_code="TRANSPORT_ERROR"), 5xx server errors (SERVER_ERRORor specific code), and uncaught exceptions during commit (COMMIT_RAISED, original chained via__cause__). No auto-release in any of these cases — the POST may have mutated Cycles before the failure.- USD-only enforcement; non-USD raises
AP2CurrencyError. Rejects NaN, +/-Infinity, and amounts with more than 8 decimal places (sub-micro precision); wraps alldecimalfailures asAP2MandateError. - Exact integer-tuple conversion in
amount_micros()— does not depend on the default decimal context, so large valid inputs convert exactly instead of being silently rounded. - Pre-allocation digit-count cap (≤ 19 integer digits, the int64 USD_MICROCENTS ceiling) blocks exponent-notation DoS like
Decimal("1E+1000000000000")that would otherwise hang allocating a trillion-digit scaling factor. - Post-conversion int64 boundary check (
micros <= 2**63 - 1) rejects amounts one over int64.max client-side instead of relying on the server. Applied symmetrically toAP2Mandate.amount_micros()andGuardedPayment.set_actual_micros()so a caller-supplied commit override cannot bypass the cap. set_actual_micros()raisesAP2MandateError(aValueErrorsubclass) for both negative and over-int64 inputs, plus any non-int type (notablyboolandfloat, which numerical comparisons would otherwise let slip through).- Direct callers of
mapping.build_commit_body(actual_micros=...)get the same input validation via a shared private validator. AP2Mandate.checkout_hashandopen_mandate_hashrequiremin_length=1when present — empty strings would otherwise silently fall back to the transaction-id lock scope.- Idempotency-key suffix filter is ASCII-only (
isascii() and isalnum()— defends against Unicode-awareisalnum()letting non-ASCII chars reach the HTTP header). AP2Mandate.from_ap2()preserves an empty upstreamcheckout_hashso the model'smin_length=1constraint can reject it (was previously masked toNoneby a falsy-orshort-circuit).- 110 tests, ≥ 95% coverage, ruff + mypy strict.
Planned for v0.2
AsyncGuardedPayment(asyncio).- Multi-currency.
payment.refundhelper.
Planned for v0.3
- Server-verifiable runtime-authority receipt (requires
cycles-protocolsigned-receipt field).