q-runtime-core 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4651 @@
1
+ """Q Runtime v0.6 full-cycle verifier and Base-Sepolia test harness.
2
+
3
+ The deterministic verifier models the x402 v2 ``authorization`` payment flow:
4
+ PAYMENT-REQUIRED -> PAYMENT-SIGNATURE -> verify -> resource -> settle -> response.
5
+
6
+ The live harness is deliberately testnet-only and localhost-only. It uses the
7
+ installed official x402 Python SDK to exercise a locally controlled resource
8
+ server against the public Base-Sepolia facilitator. It never accepts a mainnet
9
+ network and never writes private keys to reports.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Any, Mapping
19
+ from urllib.parse import urlparse
20
+
21
+ from qruntime.core.invariants import Violation
22
+ from qruntime.rails.x402 import (
23
+ X402DecodeError,
24
+ parse_payment_payload,
25
+ parse_payment_required,
26
+ parse_settlement_response,
27
+ requirement_equal,
28
+ )
29
+ from qruntime.rails.x402_capture import X402CaptureError, capture_payment_required
30
+ from qruntime.exceptions import (
31
+ DuplicateIntentError,
32
+ IntentInFlightError,
33
+ IntentResourceMismatchError,
34
+ SettlementPendingError,
35
+ PaidResourceError,
36
+ PaymentRequirementMismatchError,
37
+ PaymentRequirementRejectedError,
38
+ PolicyDeniedError,
39
+ SettlementFailedError,
40
+ SettlementReceiptError,
41
+ ReconciliationError,
42
+ SettlementStoreError,
43
+ )
44
+ from qruntime.policy import Policy, evaluate_policy
45
+
46
+
47
+ BASE_SEPOLIA = "eip155:84532"
48
+ DEFAULT_KEY_ENV = "Q_RUNTIME_TESTNET_PRIVATE_KEY"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class FullCycleResult:
53
+ passed: bool
54
+ processed_events: int
55
+ violation: Violation | None = None
56
+ trace: tuple[str, ...] = ()
57
+ pending_transactions: tuple[str, ...] = ()
58
+
59
+
60
+ def _wire(event: Mapping[str, Any]) -> Any:
61
+ if "header" in event:
62
+ return event["header"]
63
+ if "data" in event:
64
+ return event["data"]
65
+ raise X402DecodeError("full-cycle event requires data or header")
66
+
67
+
68
+ def verify_fullcycle_obj(raw: Mapping[str, Any]) -> FullCycleResult:
69
+ events = raw.get("events")
70
+ if not isinstance(events, list):
71
+ return FullCycleResult(False, 0, Violation("InvalidTrace", "events must be an array"))
72
+
73
+ flow = str(raw.get("payment_flow", "authorization"))
74
+ if flow != "authorization":
75
+ return FullCycleResult(
76
+ False,
77
+ 0,
78
+ Violation("UnsupportedPaymentFlow", "v0.6 gate models authorization flow only"),
79
+ )
80
+
81
+ states: dict[str, dict[str, Any]] = {}
82
+ trace: list[str] = []
83
+ pending: dict[str, str] = {}
84
+
85
+ def state(url: str) -> dict[str, Any]:
86
+ return states.setdefault(
87
+ url,
88
+ {
89
+ "required": None,
90
+ "accepted": None,
91
+ "authorized": False,
92
+ "verified": False,
93
+ "resource_executed": False,
94
+ "resource_failed": False,
95
+ "settled": False,
96
+ },
97
+ )
98
+
99
+ def fail(i: int, invariant: str, message: str) -> FullCycleResult:
100
+ return FullCycleResult(
101
+ False,
102
+ i,
103
+ Violation(invariant, message),
104
+ tuple(trace),
105
+ tuple(sorted(pending.values())),
106
+ )
107
+
108
+ for i, event in enumerate(events, start=1):
109
+ if not isinstance(event, Mapping):
110
+ trace.append(f"{i}:invalid")
111
+ return fail(i, "InvalidTrace", "each event must be an object")
112
+ kind = str(event.get("kind", ""))
113
+ url = str(event.get("request_url", ""))
114
+ trace.append(f"{i}:{kind}" + (f" {url}" if url else ""))
115
+
116
+ try:
117
+ if kind == "payment_required":
118
+ required = parse_payment_required(_wire(event))
119
+ target = url or required.resource_url
120
+ if not target:
121
+ return fail(i, "InvalidTrace", "payment_required requires request_url or resource.url")
122
+ st = state(target)
123
+ st["required"] = required
124
+ st["authorized"] = False
125
+ st["verified"] = False
126
+ st["resource_executed"] = False
127
+ st["resource_failed"] = False
128
+ st["settled"] = False
129
+ continue
130
+
131
+ if not url:
132
+ return fail(i, "InvalidTrace", f"{kind} requires request_url")
133
+ st = state(url)
134
+
135
+ if kind == "payment_signature":
136
+ if url in pending:
137
+ return fail(
138
+ i,
139
+ "PaymentStateReconciledBeforeRetry",
140
+ f"new authorization for {url} while prior settlement is unresolved",
141
+ )
142
+ if st["settled"]:
143
+ return fail(i, "NoDuplicateEconomicEffect", f"new payment attempted after {url} already settled")
144
+ offered = st["required"]
145
+ if offered is None:
146
+ return fail(
147
+ i,
148
+ "PaymentRequirementKnownBeforeAuthorization",
149
+ f"no current PAYMENT-REQUIRED observed for {url}",
150
+ )
151
+ payload = parse_payment_payload(_wire(event))
152
+ if payload.resource_url and payload.resource_url != url:
153
+ return fail(
154
+ i,
155
+ "PaymentPayloadBoundToResource",
156
+ f"payload resource {payload.resource_url} != actual request {url}",
157
+ )
158
+ if not any(requirement_equal(payload.accepted, req) for req in offered.accepts):
159
+ return fail(
160
+ i,
161
+ "PaymentRequirementStableForIntent",
162
+ "signed payment does not match any currently advertised payment requirement",
163
+ )
164
+ st["accepted"] = payload.accepted
165
+ st["authorized"] = True
166
+ st["verified"] = False
167
+ continue
168
+
169
+ if kind == "verify_response":
170
+ if not st["authorized"]:
171
+ return fail(i, "AuthorizationBeforeVerification", "facilitator verify occurred before authorization")
172
+ data = _wire(event)
173
+ if not isinstance(data, Mapping) or not isinstance(data.get("isValid"), bool):
174
+ return fail(i, "InvalidVerifyResponse", "verify_response requires boolean isValid")
175
+ st["verified"] = bool(data["isValid"])
176
+ if not st["verified"]:
177
+ st["authorized"] = False
178
+ continue
179
+
180
+ if kind == "resource_executed":
181
+ if not st["verified"]:
182
+ return fail(
183
+ i,
184
+ "VerificationBeforeResourceExecution",
185
+ f"protected resource {url} executed before successful verify",
186
+ )
187
+ if st["resource_executed"]:
188
+ return fail(i, "SingleResourceExecution", f"protected resource {url} executed more than once")
189
+ st["resource_executed"] = True
190
+ continue
191
+
192
+ if kind == "resource_failed":
193
+ if not st["verified"]:
194
+ return fail(
195
+ i,
196
+ "VerificationBeforeResourceExecution",
197
+ f"protected resource {url} failed before successful verify",
198
+ )
199
+ st["resource_failed"] = True
200
+ continue
201
+
202
+ if kind in {"settle_response", "payment_response"}:
203
+ if st["resource_failed"]:
204
+ return fail(
205
+ i,
206
+ "NoSettlementAfterResourceFailure",
207
+ f"authorization flow attempted settlement after protected resource {url} failed",
208
+ )
209
+ if not st["resource_executed"]:
210
+ return fail(
211
+ i,
212
+ "ResourceExecutionBeforeSettlement",
213
+ f"authorization flow settled {url} before protected resource execution",
214
+ )
215
+ response = parse_settlement_response(_wire(event))
216
+ accepted = st.get("accepted")
217
+ if accepted is not None and response.network != accepted.network:
218
+ return fail(
219
+ i,
220
+ "SettlementReceiptConsistent",
221
+ f"settlement network {response.network} != authorized network {accepted.network}",
222
+ )
223
+ if response.reconciliation_required:
224
+ pending[url] = response.transaction
225
+ st["settled"] = False
226
+ elif response.success:
227
+ if not response.transaction:
228
+ return fail(
229
+ i,
230
+ "SettlementReceiptConsistent",
231
+ "successful settlement requires a non-empty transaction hash",
232
+ )
233
+ pending.pop(url, None)
234
+ st["settled"] = True
235
+ else:
236
+ pending.pop(url, None)
237
+ st["settled"] = False
238
+ continue
239
+
240
+ if kind == "transport_timeout":
241
+ phase = str(event.get("phase", ""))
242
+ if phase in {"after_payment_submission", "during_settlement"}:
243
+ pending[url] = str(event.get("transaction", "unknown"))
244
+ continue
245
+
246
+ if kind == "reconcile":
247
+ outcome = str(event.get("outcome", ""))
248
+ if outcome not in {"confirmed", "failed", "not_found_terminal"}:
249
+ return fail(i, "InvalidTrace", "invalid reconcile outcome")
250
+ pending.pop(url, None)
251
+ st["settled"] = outcome == "confirmed"
252
+ continue
253
+
254
+ if kind == "retry":
255
+ if url in pending:
256
+ return fail(
257
+ i,
258
+ "PaymentStateReconciledBeforeRetry",
259
+ f"retry attempted for {url} before ambiguous settlement was reconciled",
260
+ )
261
+ if st["settled"]:
262
+ return fail(i, "NoDuplicateEconomicEffect", f"retry attempted after {url} already settled")
263
+ continue
264
+
265
+ return fail(i, "InvalidTrace", f"unsupported full-cycle event kind: {kind}")
266
+
267
+ except X402DecodeError as exc:
268
+ return fail(i, "InvalidX402WireData", str(exc))
269
+
270
+ return FullCycleResult(True, len(events), None, tuple(trace), tuple(sorted(pending.values())))
271
+
272
+
273
+ def verify_fullcycle(path: str | Path) -> FullCycleResult:
274
+ raw = json.loads(Path(path).read_text(encoding="utf-8"))
275
+ if not isinstance(raw, Mapping):
276
+ return FullCycleResult(False, 0, Violation("InvalidTrace", "trace root must be an object"))
277
+ return verify_fullcycle_obj(raw)
278
+
279
+
280
+ def fullcycle_result_dict(result: FullCycleResult) -> dict[str, Any]:
281
+ return {
282
+ "passed": result.passed,
283
+ "processed_events": result.processed_events,
284
+ "violation": None if result.violation is None else {
285
+ "invariant": result.violation.invariant,
286
+ "message": result.violation.message,
287
+ },
288
+ "trace": list(result.trace),
289
+ "pending_transactions": list(result.pending_transactions),
290
+ }
291
+
292
+
293
+ def fullcycle_result_text(result: FullCycleResult) -> str:
294
+ if result.passed:
295
+ pending_text = ", ".join(result.pending_transactions) if result.pending_transactions else "none"
296
+ return (
297
+ "PASS — X402 FULL-CYCLE INVARIANTS\n"
298
+ f"events processed: {result.processed_events}\n"
299
+ f"pending reconciliation: {pending_text}\n"
300
+ "economic invariant violations: 0"
301
+ )
302
+ return (
303
+ "FAIL — X402_FULLCYCLE_COUNTEREXAMPLE\n"
304
+ f"invariant: {result.violation.invariant if result.violation else 'unknown'}\n"
305
+ f"reason: {result.violation.message if result.violation else 'unknown'}\n"
306
+ "trace:\n " + "\n ".join(result.trace)
307
+ )
308
+
309
+
310
+ def create_testnet_wallet(path: str | Path) -> dict[str, str]:
311
+ """Create a disposable EVM wallet file for testnet use only."""
312
+ try:
313
+ from eth_account import Account
314
+ except ImportError as exc: # pragma: no cover - user's optional integration env
315
+ raise RuntimeError(
316
+ "eth-account is missing. Install Q Runtime with: "
317
+ 'python -m pip install -e ".[official-x402]"'
318
+ ) from exc
319
+
320
+ destination = Path(path)
321
+ if destination.exists():
322
+ raise FileExistsError(f"refusing to overwrite existing wallet file: {destination}")
323
+ destination.parent.mkdir(parents=True, exist_ok=True)
324
+ account = Account.create()
325
+ private_key = account.key.hex()
326
+ if not private_key.startswith("0x"):
327
+ private_key = "0x" + private_key
328
+ data = {
329
+ "purpose": "Q Runtime Base Sepolia disposable testnet wallet",
330
+ "network": BASE_SEPOLIA,
331
+ "address": account.address,
332
+ "private_key": private_key,
333
+ "warning": "TESTNET ONLY. Never fund this address with mainnet assets or reuse this key on mainnet.",
334
+ }
335
+ destination.write_text(json.dumps(data, indent=2), encoding="utf-8")
336
+ return {"address": account.address, "path": str(destination)}
337
+
338
+
339
+ def _header_ci(headers: Mapping[str, str], *names: str) -> str | None:
340
+ lowered = {str(k).lower(): str(v) for k, v in headers.items()}
341
+ for name in names:
342
+ if name.lower() in lowered:
343
+ return lowered[name.lower()]
344
+ return None
345
+
346
+
347
+
348
+ def _live_requirement_matches_preflight(preflight: Any, current: Any) -> bool:
349
+ """Require the SDK-selected requirement to equal the preflight-approved quote."""
350
+ try:
351
+ current_canonical = {
352
+ "scheme": current.scheme,
353
+ "network": current.network,
354
+ "amount": str(current.amount),
355
+ "asset": current.asset,
356
+ "payTo": current.pay_to,
357
+ "maxTimeoutSeconds": current.max_timeout_seconds,
358
+ "extra": dict(current.extra or {}),
359
+ }
360
+ return preflight.canonical_dict() == current_canonical
361
+ except (AttributeError, TypeError, ValueError):
362
+ return False
363
+
364
+ def _make_live_quote_binding_hook(preflight: Any, abort_result_cls: Any):
365
+ """Abort before signing if the live SDK quote differs from preflight."""
366
+ fingerprint = preflight.fingerprint()
367
+
368
+ def before_payment(ctx: Any):
369
+ current = ctx.selected_requirements
370
+ if not _live_requirement_matches_preflight(preflight, current):
371
+ return abort_result_cls(
372
+ reason="qruntime_preflight_quote_changed",
373
+ message=(
374
+ "payment requirement selected for signing no longer "
375
+ f"matches preflight-approved quote {fingerprint}"
376
+ ),
377
+ )
378
+ return None
379
+
380
+ return before_payment
381
+
382
+
383
+
384
+ PENDING_STORE_ENV = "Q_RUNTIME_PENDING_STORE"
385
+ PENDING_STORE_FORMAT = "qruntime-x402-pending-store-v1"
386
+ PENDING_STORE_ENTRY_STATES = frozenset(
387
+ {
388
+ "reserved",
389
+ "submission_unknown",
390
+ "pending",
391
+ "resolved_confirmed",
392
+ "resolved_failed",
393
+ }
394
+ )
395
+
396
+
397
+ def _pending_store_file() -> Path:
398
+ configured = os.environ.get(PENDING_STORE_ENV, "").strip()
399
+ if configured:
400
+ return Path(configured)
401
+ return Path.home() / ".qruntime" / "pending-settlements.json"
402
+
403
+
404
+ def _pending_store_temp_file(
405
+ path: Path,
406
+ ) -> Path:
407
+ return path.with_name(
408
+ path.name + ".tmp"
409
+ )
410
+
411
+
412
+ def _pending_store_is_unambiguously_absent(
413
+ path: Path,
414
+ ) -> bool:
415
+ """Return True only when neither main nor temp state exists."""
416
+
417
+ temp = _pending_store_temp_file(
418
+ path
419
+ )
420
+
421
+ if temp.exists():
422
+ raise SettlementStoreError(
423
+ "persistent settlement store has orphaned temp state; "
424
+ "refusing payment fail-closed"
425
+ )
426
+
427
+ return not path.exists()
428
+
429
+
430
+
431
+
432
+ def _read_pending_store_document(
433
+ path: Path,
434
+ ) -> dict[str, Any]:
435
+ """Read and structurally validate one persisted pending store."""
436
+
437
+ try:
438
+ raw = json.loads(
439
+ path.read_text(
440
+ encoding="utf-8"
441
+ )
442
+ )
443
+ except (
444
+ OSError,
445
+ UnicodeDecodeError,
446
+ json.JSONDecodeError,
447
+ ) as exc:
448
+ raise SettlementStoreError(
449
+ "persistent settlement store is unreadable; "
450
+ "refusing payment fail-closed"
451
+ ) from exc
452
+
453
+ if not isinstance(
454
+ raw,
455
+ Mapping,
456
+ ):
457
+ raise SettlementStoreError(
458
+ "persistent settlement store root must be an object"
459
+ )
460
+
461
+ entries = raw.get(
462
+ "entries"
463
+ )
464
+
465
+ if not isinstance(
466
+ entries,
467
+ Mapping,
468
+ ):
469
+ raise SettlementStoreError(
470
+ "persistent settlement store entries must be an object"
471
+ )
472
+
473
+ for outer_key, entry in entries.items():
474
+ if not isinstance(
475
+ entry,
476
+ Mapping,
477
+ ):
478
+ raise SettlementStoreError(
479
+ "persistent settlement store entry must be an object"
480
+ )
481
+
482
+ state = entry.get(
483
+ "state"
484
+ )
485
+
486
+ if (
487
+ not isinstance(
488
+ state,
489
+ str,
490
+ )
491
+ or state
492
+ not in PENDING_STORE_ENTRY_STATES
493
+ ):
494
+ raise SettlementStoreError(
495
+ "persistent settlement store entry state is unsupported"
496
+ )
497
+
498
+ stored_intent_key = entry.get(
499
+ "intent_key"
500
+ )
501
+
502
+ # Legacy entries may omit intent_key.
503
+ # Once present, it is an integrity assertion.
504
+ if stored_intent_key is not None:
505
+ if (
506
+ not isinstance(
507
+ stored_intent_key,
508
+ str,
509
+ )
510
+ or stored_intent_key
511
+ != outer_key
512
+ ):
513
+ raise SettlementStoreError(
514
+ "persistent settlement store entry key is inconsistent"
515
+ )
516
+
517
+ stored_intent_id = entry.get(
518
+ "intent_id"
519
+ )
520
+
521
+ if stored_intent_id is not None:
522
+ if (
523
+ not isinstance(
524
+ stored_intent_id,
525
+ str,
526
+ )
527
+ or _persistent_intent_key(
528
+ stored_intent_id
529
+ )
530
+ != stored_intent_key
531
+ ):
532
+ raise SettlementStoreError(
533
+ "persistent settlement store entry intent identity is inconsistent"
534
+ )
535
+
536
+ # A persisted pending settlement is already economically
537
+ # significant. Its transaction identity must therefore remain
538
+ # recoverable; otherwise retry safety cannot be established.
539
+ for raw_entry in entries.values():
540
+ if not isinstance(
541
+ raw_entry,
542
+ Mapping,
543
+ ):
544
+ continue
545
+
546
+ if raw_entry.get(
547
+ "state"
548
+ ) != "pending":
549
+ continue
550
+
551
+ for field in (
552
+ "transaction",
553
+ "network",
554
+ ):
555
+ value = raw_entry.get(
556
+ field
557
+ )
558
+
559
+ if (
560
+ not isinstance(
561
+ value,
562
+ str,
563
+ )
564
+ or not value.strip()
565
+ ):
566
+ raise SettlementStoreError(
567
+ "persistent pending settlement entry "
568
+ f"requires non-empty {field}; "
569
+ "refusing payment fail-closed"
570
+ )
571
+
572
+ # Terminal reconciliation states must retain the
573
+ # economic identity that was reconciled, and the persisted
574
+ # outcome must agree with the terminal state.
575
+ for raw_entry in entries.values():
576
+ if not isinstance(
577
+ raw_entry,
578
+ Mapping,
579
+ ):
580
+ continue
581
+
582
+ state = raw_entry.get(
583
+ "state"
584
+ )
585
+
586
+ expected_outcome = {
587
+ "resolved_confirmed": "confirmed",
588
+ "resolved_failed": "failed",
589
+ }.get(
590
+ state
591
+ )
592
+
593
+ if expected_outcome is None:
594
+ continue
595
+
596
+ for field in (
597
+ "transaction",
598
+ "network",
599
+ ):
600
+ value = raw_entry.get(
601
+ field
602
+ )
603
+
604
+ if (
605
+ not isinstance(
606
+ value,
607
+ str,
608
+ )
609
+ or not value.strip()
610
+ ):
611
+ raise SettlementStoreError(
612
+ "persistent resolved settlement entry "
613
+ f"requires non-empty {field}; "
614
+ "refusing payment fail-closed"
615
+ )
616
+
617
+ outcome = raw_entry.get(
618
+ "reconciliation_outcome"
619
+ )
620
+
621
+ # Older v1 entries may not carry reconciliation_outcome.
622
+ # Absence remains readable for compatibility. Once present,
623
+ # however, the outcome is an integrity assertion and must
624
+ # agree with the terminal state.
625
+ if (
626
+ outcome is not None
627
+ and outcome != expected_outcome
628
+ ):
629
+ raise SettlementStoreError(
630
+ "persistent resolved settlement entry "
631
+ "has reconciliation outcome inconsistent "
632
+ f"with state {state}; "
633
+ "refusing payment fail-closed"
634
+ )
635
+
636
+ # P4.7c persisted authorization nonce integrity
637
+ # The write path already treats a nonce as unique within one
638
+ # persisted intent history. Exact replay is idempotent; conflicting
639
+ # durable envelopes for the same nonce are corruption.
640
+ for raw_entry in entries.values():
641
+ if not isinstance(
642
+ raw_entry,
643
+ Mapping,
644
+ ):
645
+ continue
646
+
647
+ if raw_entry.get(
648
+ "state"
649
+ ) != "submission_unknown":
650
+ continue
651
+
652
+ history_raw = raw_entry.get(
653
+ "authorizations"
654
+ )
655
+
656
+ # P4.7d authorization history structural integrity
657
+ # Missing/null history is legitimate while submission_unknown
658
+ # exists before authorization evidence reaches durable storage.
659
+ if history_raw is None:
660
+ continue
661
+
662
+ # Once materially present, authorizations must retain
663
+ # the list shape established by the write path.
664
+ if not isinstance(
665
+ history_raw,
666
+ list,
667
+ ):
668
+ raise SettlementStoreError(
669
+ "persistent authorization history "
670
+ "must be a list; refusing payment "
671
+ "fail-closed"
672
+ )
673
+
674
+ seen_by_nonce: dict[
675
+ str,
676
+ dict[str, str],
677
+ ] = {}
678
+
679
+ for authorization in history_raw:
680
+ if not isinstance(
681
+ authorization,
682
+ Mapping,
683
+ ):
684
+ raise SettlementStoreError(
685
+ "persistent authorization history "
686
+ "contains invalid entry; refusing "
687
+ "payment fail-closed"
688
+ )
689
+
690
+ # P4.7f authorization envelope completeness
691
+ # Modern persisted authorization history must retain every
692
+ # field produced by the durable writer. This mirrors the
693
+ # write-time contract without imposing additional typing.
694
+ required_fields = (
695
+ "from",
696
+ "to",
697
+ "value",
698
+ "validAfter",
699
+ "validBefore",
700
+ "nonce",
701
+ "network",
702
+ "asset",
703
+ )
704
+
705
+ for field in required_fields:
706
+ if authorization.get(
707
+ field
708
+ ) is None:
709
+ raise SettlementStoreError(
710
+ "persistent authorization envelope "
711
+ f"is missing required field {field}; "
712
+ "refusing payment fail-closed"
713
+ )
714
+
715
+ # P4.7g authorization economic binding
716
+ # When multiple persisted representations explicitly carry
717
+ # the same economic dimensions, they must agree. Missing
718
+ # legacy representations remain outside this contract.
719
+ requirement_raw = raw_entry.get(
720
+ "requirement"
721
+ )
722
+
723
+ if isinstance(
724
+ requirement_raw,
725
+ Mapping,
726
+ ):
727
+ entry_network = raw_entry.get(
728
+ "network"
729
+ )
730
+
731
+ requirement_network = (
732
+ requirement_raw.get(
733
+ "network"
734
+ )
735
+ )
736
+
737
+ if (
738
+ entry_network is not None
739
+ and requirement_network is not None
740
+ and str(entry_network)
741
+ != str(requirement_network)
742
+ ):
743
+ raise SettlementStoreError(
744
+ "persistent submission_unknown network "
745
+ "is inconsistent with payment requirement; "
746
+ "refusing payment fail-closed"
747
+ )
748
+
749
+ authorization_network = (
750
+ authorization.get(
751
+ "network"
752
+ )
753
+ )
754
+
755
+ if (
756
+ entry_network is not None
757
+ and authorization_network is not None
758
+ and str(authorization_network)
759
+ != str(entry_network)
760
+ ):
761
+ raise SettlementStoreError(
762
+ "persistent authorization network "
763
+ "is inconsistent with intent network; "
764
+ "refusing payment fail-closed"
765
+ )
766
+
767
+ requirement_asset = (
768
+ requirement_raw.get(
769
+ "asset"
770
+ )
771
+ )
772
+
773
+ authorization_asset = (
774
+ authorization.get(
775
+ "asset"
776
+ )
777
+ )
778
+
779
+ if (
780
+ requirement_asset is not None
781
+ and authorization_asset is not None
782
+ and str(authorization_asset)
783
+ != str(requirement_asset)
784
+ ):
785
+ raise SettlementStoreError(
786
+ "persistent authorization asset "
787
+ "is inconsistent with payment requirement; "
788
+ "refusing payment fail-closed"
789
+ )
790
+
791
+
792
+ # P4.7h authorization transfer binding
793
+ # For the exact EVM authorization flow, the durable
794
+ # authorization must describe the same amount and recipient
795
+ # as the selected payment requirement.
796
+ if isinstance(
797
+ requirement_raw,
798
+ Mapping,
799
+ ):
800
+ scheme = requirement_raw.get(
801
+ "scheme"
802
+ )
803
+
804
+ if str(
805
+ scheme
806
+ ) == "exact":
807
+ requirement_amount = (
808
+ requirement_raw.get(
809
+ "amount"
810
+ )
811
+ )
812
+
813
+ authorization_value = (
814
+ authorization.get(
815
+ "value"
816
+ )
817
+ )
818
+
819
+ if (
820
+ requirement_amount is not None
821
+ and authorization_value is not None
822
+ and str(
823
+ authorization_value
824
+ )
825
+ != str(
826
+ requirement_amount
827
+ )
828
+ ):
829
+ raise SettlementStoreError(
830
+ "persistent authorization value "
831
+ "is inconsistent with exact "
832
+ "payment requirement amount; "
833
+ "refusing payment fail-closed"
834
+ )
835
+
836
+ requirement_pay_to = (
837
+ requirement_raw.get(
838
+ "payTo"
839
+ )
840
+ )
841
+
842
+ authorization_to = (
843
+ authorization.get(
844
+ "to"
845
+ )
846
+ )
847
+
848
+ if (
849
+ requirement_pay_to is not None
850
+ and authorization_to is not None
851
+ and str(
852
+ authorization_to
853
+ ).lower()
854
+ != str(
855
+ requirement_pay_to
856
+ ).lower()
857
+ ):
858
+ raise SettlementStoreError(
859
+ "persistent authorization recipient "
860
+ "is inconsistent with exact "
861
+ "payment requirement payTo; "
862
+ "refusing payment fail-closed"
863
+ )
864
+
865
+
866
+ nonce_raw = authorization.get(
867
+ "nonce"
868
+ )
869
+
870
+ if nonce_raw is None:
871
+ continue
872
+
873
+ nonce = str(
874
+ nonce_raw
875
+ )
876
+
877
+ normalized = {
878
+ str(key): str(value)
879
+ for key, value
880
+ in authorization.items()
881
+ }
882
+
883
+ previous = seen_by_nonce.get(
884
+ nonce
885
+ )
886
+
887
+ if (
888
+ previous is not None
889
+ and previous != normalized
890
+ ):
891
+ raise SettlementStoreError(
892
+ "persistent authorization history "
893
+ "contains conflicting envelopes for "
894
+ "the same nonce; refusing payment "
895
+ "fail-closed"
896
+ )
897
+
898
+ seen_by_nonce[
899
+ nonce
900
+ ] = normalized
901
+
902
+ # P4.7e authorization alias coherence
903
+ # The singular authorization field is a backwards-compatible
904
+ # alias for the most recently persisted authorization envelope.
905
+ # Its absence remains valid. Once explicitly present alongside
906
+ # non-empty history, however, both representations must agree.
907
+ for raw_entry in entries.values():
908
+ if not isinstance(
909
+ raw_entry,
910
+ Mapping,
911
+ ):
912
+ continue
913
+
914
+ if raw_entry.get(
915
+ "state"
916
+ ) != "submission_unknown":
917
+ continue
918
+
919
+ history_raw = raw_entry.get(
920
+ "authorizations"
921
+ )
922
+
923
+ if (
924
+ not isinstance(
925
+ history_raw,
926
+ list,
927
+ )
928
+ or not history_raw
929
+ ):
930
+ continue
931
+
932
+ if "authorization" not in raw_entry:
933
+ continue
934
+
935
+ alias = raw_entry.get(
936
+ "authorization"
937
+ )
938
+
939
+ if not isinstance(
940
+ alias,
941
+ Mapping,
942
+ ):
943
+ raise SettlementStoreError(
944
+ "persistent authorization alias "
945
+ "must be an object when history exists; "
946
+ "refusing payment fail-closed"
947
+ )
948
+
949
+ latest = history_raw[-1]
950
+
951
+ if not isinstance(
952
+ latest,
953
+ Mapping,
954
+ ):
955
+ raise SettlementStoreError(
956
+ "persistent authorization history "
957
+ "contains invalid latest entry; "
958
+ "refusing payment fail-closed"
959
+ )
960
+
961
+ normalized_alias = {
962
+ str(key): str(value)
963
+ for key, value
964
+ in alias.items()
965
+ }
966
+
967
+ normalized_latest = {
968
+ str(key): str(value)
969
+ for key, value
970
+ in latest.items()
971
+ }
972
+
973
+ if (
974
+ normalized_alias
975
+ != normalized_latest
976
+ ):
977
+ raise SettlementStoreError(
978
+ "persistent authorization alias "
979
+ "is inconsistent with latest history; "
980
+ "refusing payment fail-closed"
981
+ )
982
+
983
+ # P4.7i reservation scope binding
984
+ # A flat durable budget reservation preserves the scope checked
985
+ # before economic handoff. When duplicated at entry/requirement
986
+ # level, those persisted dimensions must remain coherent.
987
+ for raw_entry in entries.values():
988
+ if not isinstance(
989
+ raw_entry,
990
+ Mapping,
991
+ ):
992
+ continue
993
+
994
+ reservation_raw = raw_entry.get(
995
+ "reservation"
996
+ )
997
+
998
+ if not isinstance(
999
+ reservation_raw,
1000
+ Mapping,
1001
+ ):
1002
+ continue
1003
+
1004
+ # BudgetSet stores constraints[] instead of one flat scope.
1005
+ # Its contract is deliberately outside P4.7i.
1006
+ if "constraints" in reservation_raw:
1007
+ continue
1008
+
1009
+ requirement_raw = raw_entry.get(
1010
+ "requirement"
1011
+ )
1012
+
1013
+ if isinstance(
1014
+ requirement_raw,
1015
+ Mapping,
1016
+ ):
1017
+ reservation_asset = (
1018
+ reservation_raw.get(
1019
+ "asset"
1020
+ )
1021
+ )
1022
+
1023
+ requirement_asset = (
1024
+ requirement_raw.get(
1025
+ "asset"
1026
+ )
1027
+ )
1028
+
1029
+ if (
1030
+ reservation_asset is not None
1031
+ and requirement_asset is not None
1032
+ and str(
1033
+ reservation_asset
1034
+ )
1035
+ != str(
1036
+ requirement_asset
1037
+ )
1038
+ ):
1039
+ raise SettlementStoreError(
1040
+ "persistent reservation asset "
1041
+ "is inconsistent with payment requirement; "
1042
+ "refusing payment fail-closed"
1043
+ )
1044
+
1045
+ for field in (
1046
+ "agent_id",
1047
+ "session_id",
1048
+ ):
1049
+ reservation_scope = (
1050
+ reservation_raw.get(
1051
+ field
1052
+ )
1053
+ )
1054
+
1055
+ entry_scope = raw_entry.get(
1056
+ field
1057
+ )
1058
+
1059
+ if (
1060
+ reservation_scope is not None
1061
+ and entry_scope is not None
1062
+ and str(
1063
+ reservation_scope
1064
+ )
1065
+ != str(
1066
+ entry_scope
1067
+ )
1068
+ ):
1069
+ raise SettlementStoreError(
1070
+ "persistent reservation "
1071
+ f"{field} is inconsistent with "
1072
+ "payment entry scope; "
1073
+ "refusing payment fail-closed"
1074
+ )
1075
+
1076
+ reservation_origin = (
1077
+ reservation_raw.get(
1078
+ "origin"
1079
+ )
1080
+ )
1081
+
1082
+ resource = raw_entry.get(
1083
+ "resource"
1084
+ )
1085
+
1086
+ if (
1087
+ reservation_origin is not None
1088
+ and resource is not None
1089
+ ):
1090
+ from qruntime.policy import (
1091
+ _normalize_origin,
1092
+ )
1093
+
1094
+ try:
1095
+ resource_origin = (
1096
+ _normalize_origin(
1097
+ str(
1098
+ resource
1099
+ )
1100
+ )
1101
+ )
1102
+ except ValueError as exc:
1103
+ raise SettlementStoreError(
1104
+ "persistent reservation resource "
1105
+ "has invalid origin; refusing "
1106
+ "payment fail-closed"
1107
+ ) from exc
1108
+
1109
+ if (
1110
+ str(
1111
+ reservation_origin
1112
+ )
1113
+ != resource_origin
1114
+ ):
1115
+ raise SettlementStoreError(
1116
+ "persistent reservation origin "
1117
+ "is inconsistent with payment resource; "
1118
+ "refusing payment fail-closed"
1119
+ )
1120
+
1121
+ return dict(raw)
1122
+
1123
+ def _classify_pending_store_format(
1124
+ raw: Mapping[str, Any],
1125
+ ) -> str:
1126
+ """Classify a validated pending-store document."""
1127
+
1128
+ if "format" not in raw:
1129
+ return "legacy"
1130
+
1131
+ if raw["format"] != PENDING_STORE_FORMAT:
1132
+ raise SettlementStoreError(
1133
+ "persistent settlement store format is unsupported; "
1134
+ "refusing payment fail-closed"
1135
+ )
1136
+
1137
+ return "current"
1138
+
1139
+
1140
+ def _load_pending_store() -> dict[str, Any]:
1141
+ path = _pending_store_file()
1142
+
1143
+ if _pending_store_is_unambiguously_absent(
1144
+ path
1145
+ ):
1146
+ return {
1147
+ "format": PENDING_STORE_FORMAT,
1148
+ "entries": {},
1149
+ }
1150
+
1151
+ raw = _read_pending_store_document(
1152
+ path
1153
+ )
1154
+
1155
+ _classify_pending_store_format(
1156
+ raw
1157
+ )
1158
+
1159
+ return {
1160
+ "format": PENDING_STORE_FORMAT,
1161
+ "entries": dict(
1162
+ raw["entries"]
1163
+ ),
1164
+ }
1165
+
1166
+
1167
+
1168
+
1169
+
1170
+
1171
+ def _write_pending_store(store: Mapping[str, Any]) -> None:
1172
+ path = _pending_store_file()
1173
+ path.parent.mkdir(parents=True, exist_ok=True)
1174
+
1175
+ temp = _pending_store_temp_file(
1176
+ path
1177
+ )
1178
+ payload = json.dumps(
1179
+ store,
1180
+ indent=2,
1181
+ ensure_ascii=False,
1182
+ )
1183
+
1184
+ with temp.open(
1185
+ "w",
1186
+ encoding="utf-8",
1187
+ ) as handle:
1188
+ handle.write(payload)
1189
+ handle.flush()
1190
+ os.fsync(handle.fileno())
1191
+
1192
+ temp.replace(path)
1193
+
1194
+
1195
+ def _pending_store_transaction_lock_file() -> Path:
1196
+ store = _pending_store_file()
1197
+ return store.with_name(store.name + ".transaction.lock")
1198
+
1199
+
1200
+ def _acquire_pending_store_transaction_lock(
1201
+ timeout: float = 5.0,
1202
+ ) -> tuple[Path, int]:
1203
+ """Acquire an OS-backed cross-process lock for store mutation."""
1204
+ import time
1205
+
1206
+ if timeout <= 0:
1207
+ raise ValueError("store lock timeout must be positive")
1208
+
1209
+ path = _pending_store_transaction_lock_file()
1210
+ path.parent.mkdir(parents=True, exist_ok=True)
1211
+
1212
+ # The lock file itself may exist forever. Existence is NOT ownership.
1213
+ flags = os.O_RDWR | os.O_CREAT
1214
+ flags |= getattr(os, "O_BINARY", 0)
1215
+ flags |= getattr(os, "O_NOINHERIT", 0)
1216
+
1217
+ fd = os.open(path, flags, 0o600)
1218
+
1219
+ try:
1220
+ # Windows byte-range locking needs a byte to lock.
1221
+ if os.fstat(fd).st_size == 0:
1222
+ os.lseek(fd, 0, os.SEEK_SET)
1223
+ os.write(fd, b"\0")
1224
+ os.fsync(fd)
1225
+
1226
+ deadline = time.monotonic() + timeout
1227
+
1228
+ if os.name == "nt":
1229
+ import msvcrt
1230
+
1231
+ while True:
1232
+ os.lseek(fd, 0, os.SEEK_SET)
1233
+
1234
+ try:
1235
+ msvcrt.locking(
1236
+ fd,
1237
+ msvcrt.LK_NBLCK,
1238
+ 1,
1239
+ )
1240
+ break
1241
+
1242
+ except OSError as exc:
1243
+ if time.monotonic() >= deadline:
1244
+ raise SettlementStoreError(
1245
+ "persistent settlement store is busy; "
1246
+ "refusing mutation fail-closed"
1247
+ ) from exc
1248
+
1249
+ time.sleep(0.01)
1250
+
1251
+ else:
1252
+ import errno
1253
+ import fcntl
1254
+
1255
+ while True:
1256
+ try:
1257
+ fcntl.flock(
1258
+ fd,
1259
+ fcntl.LOCK_EX | fcntl.LOCK_NB,
1260
+ )
1261
+ break
1262
+
1263
+ except OSError as exc:
1264
+ if exc.errno not in {
1265
+ errno.EACCES,
1266
+ errno.EAGAIN,
1267
+ }:
1268
+ raise
1269
+
1270
+ if time.monotonic() >= deadline:
1271
+ raise SettlementStoreError(
1272
+ "persistent settlement store is busy; "
1273
+ "refusing mutation fail-closed"
1274
+ ) from exc
1275
+
1276
+ time.sleep(0.01)
1277
+
1278
+ return path, fd
1279
+
1280
+ except Exception:
1281
+ os.close(fd)
1282
+ raise
1283
+
1284
+
1285
+ def _release_pending_store_transaction_lock(
1286
+ handle: tuple[Path, int],
1287
+ ) -> None:
1288
+ _path, fd = handle
1289
+
1290
+ try:
1291
+ if os.name == "nt":
1292
+ import msvcrt
1293
+
1294
+ os.lseek(fd, 0, os.SEEK_SET)
1295
+ msvcrt.locking(
1296
+ fd,
1297
+ msvcrt.LK_UNLCK,
1298
+ 1,
1299
+ )
1300
+
1301
+ else:
1302
+ import fcntl
1303
+
1304
+ fcntl.flock(
1305
+ fd,
1306
+ fcntl.LOCK_UN,
1307
+ )
1308
+
1309
+ finally:
1310
+ os.close(fd)
1311
+
1312
+
1313
+ def _migrate_pending_store_to_current_format() -> bool:
1314
+ """Migrate one legacy versionless pending store to current format."""
1315
+
1316
+ lock = _acquire_pending_store_transaction_lock()
1317
+
1318
+ try:
1319
+ path = _pending_store_file()
1320
+
1321
+ if _pending_store_is_unambiguously_absent(
1322
+ path
1323
+ ):
1324
+ return False
1325
+
1326
+ raw = _read_pending_store_document(
1327
+ path
1328
+ )
1329
+
1330
+ format_kind = (
1331
+ _classify_pending_store_format(
1332
+ raw
1333
+ )
1334
+ )
1335
+
1336
+ if format_kind == "current":
1337
+ # Already current: preserve bytes exactly.
1338
+ return False
1339
+
1340
+ migrated = {
1341
+ "format": PENDING_STORE_FORMAT,
1342
+ "entries": dict(
1343
+ raw["entries"]
1344
+ ),
1345
+ }
1346
+
1347
+ _write_pending_store(
1348
+ migrated
1349
+ )
1350
+
1351
+ return True
1352
+
1353
+ finally:
1354
+ _release_pending_store_transaction_lock(
1355
+ lock
1356
+ )
1357
+
1358
+
1359
+
1360
+
1361
+ def _mutate_pending_store(mutator: Any) -> Any:
1362
+ """Perform one atomic load-mutate-write store transaction."""
1363
+ lock = _acquire_pending_store_transaction_lock()
1364
+
1365
+ try:
1366
+ store = _load_pending_store()
1367
+
1368
+ result, changed = mutator(store)
1369
+
1370
+ if changed:
1371
+ _write_pending_store(store)
1372
+
1373
+ return result
1374
+
1375
+ finally:
1376
+ _release_pending_store_transaction_lock(lock)
1377
+
1378
+
1379
+ def _persistent_intent_key(intent_id: str) -> str:
1380
+ import hashlib
1381
+
1382
+ return hashlib.sha256(intent_id.encode("utf-8")).hexdigest()
1383
+
1384
+
1385
+
1386
+ def list_persistent_payment_intents() -> tuple[dict[str, Any], ...]:
1387
+ """Return a consistent read-only snapshot of persisted payment intents."""
1388
+
1389
+ def read(store: dict[str, Any]):
1390
+ entries = [
1391
+ dict(raw_entry)
1392
+ for raw_entry in store["entries"].values()
1393
+ if isinstance(raw_entry, Mapping)
1394
+ ]
1395
+
1396
+ entries.sort(
1397
+ key=lambda entry: (
1398
+ str(entry.get("updated_at", "")),
1399
+ str(entry.get("intent_id", "")),
1400
+ ),
1401
+ reverse=True,
1402
+ )
1403
+
1404
+ return tuple(entries), False
1405
+
1406
+ return _mutate_pending_store(read)
1407
+
1408
+
1409
+ def get_persistent_payment_intent(
1410
+ intent_id: str,
1411
+ ) -> dict[str, Any] | None:
1412
+ """Return one persisted logical payment intent without mutating state."""
1413
+
1414
+ normalized = intent_id.strip()
1415
+
1416
+ if not normalized:
1417
+ raise ValueError("intent_id must not be empty")
1418
+
1419
+ if len(normalized) > 256:
1420
+ raise ValueError("intent_id must be at most 256 characters")
1421
+
1422
+ intent_key = _persistent_intent_key(normalized)
1423
+
1424
+ def read(store: dict[str, Any]):
1425
+ raw_entry = store["entries"].get(intent_key)
1426
+
1427
+ if not isinstance(raw_entry, Mapping):
1428
+ return None, False
1429
+
1430
+ return dict(raw_entry), False
1431
+
1432
+ return _mutate_pending_store(read)
1433
+
1434
+
1435
+ def _resolve_payment_intent_id(
1436
+ intent_id: str | None,
1437
+ resource: str,
1438
+ requirement: Any,
1439
+ ) -> str:
1440
+ """Return a stable logical operation id.
1441
+
1442
+ Explicit intent ids survive quote changes. Legacy callers fall back to
1443
+ resource + quote fingerprint for backward compatibility.
1444
+ """
1445
+ import hashlib
1446
+
1447
+ if intent_id is not None:
1448
+ normalized = intent_id.strip()
1449
+
1450
+ if not normalized:
1451
+ raise ValueError("intent_id must not be empty")
1452
+
1453
+ if len(normalized) > 256:
1454
+ raise ValueError("intent_id must be at most 256 characters")
1455
+
1456
+ return normalized
1457
+
1458
+ legacy_raw = (
1459
+ f"{resource}\n{requirement.fingerprint()}".encode("utf-8")
1460
+ )
1461
+ legacy_digest = hashlib.sha256(legacy_raw).hexdigest()
1462
+ return f"legacy:{legacy_digest}"
1463
+
1464
+
1465
+
1466
+ def _intent_claim_directory() -> Path:
1467
+ store = _pending_store_file()
1468
+ return store.parent / f"{store.name}.claims"
1469
+
1470
+
1471
+ def _probe_process_identity(
1472
+ pid: int,
1473
+ ) -> dict[str, str | None]:
1474
+ """Probe one process without sending signals.
1475
+
1476
+ Returns:
1477
+ - alive: process exists and has a stable creation identity;
1478
+ - missing: process is proven absent/terminated;
1479
+ - indeterminate: existence/identity could not be proven safely.
1480
+
1481
+ PID alone is insufficient because operating systems may reuse it.
1482
+ """
1483
+
1484
+ import sys
1485
+
1486
+ if (
1487
+ isinstance(pid, bool)
1488
+ or not isinstance(pid, int)
1489
+ ):
1490
+ return {
1491
+ "status": "indeterminate",
1492
+ "identity": None,
1493
+ }
1494
+
1495
+ if pid <= 0:
1496
+ return {
1497
+ "status": "missing",
1498
+ "identity": None,
1499
+ }
1500
+
1501
+ # ---------------------------------------------------------
1502
+ # Windows
1503
+ # ---------------------------------------------------------
1504
+
1505
+ if os.name == "nt":
1506
+ import ctypes
1507
+ from ctypes import wintypes
1508
+
1509
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
1510
+ SYNCHRONIZE = 0x00100000
1511
+
1512
+ WAIT_OBJECT_0 = 0x00000000
1513
+ WAIT_TIMEOUT = 0x00000102
1514
+ WAIT_FAILED = 0xFFFFFFFF
1515
+
1516
+ ERROR_INVALID_PARAMETER = 87
1517
+
1518
+ kernel32 = ctypes.WinDLL(
1519
+ "kernel32",
1520
+ use_last_error=True,
1521
+ )
1522
+
1523
+ open_process = kernel32.OpenProcess
1524
+ open_process.argtypes = (
1525
+ wintypes.DWORD,
1526
+ wintypes.BOOL,
1527
+ wintypes.DWORD,
1528
+ )
1529
+ open_process.restype = wintypes.HANDLE
1530
+
1531
+ wait_for_single_object = (
1532
+ kernel32.WaitForSingleObject
1533
+ )
1534
+ wait_for_single_object.argtypes = (
1535
+ wintypes.HANDLE,
1536
+ wintypes.DWORD,
1537
+ )
1538
+ wait_for_single_object.restype = (
1539
+ wintypes.DWORD
1540
+ )
1541
+
1542
+ get_process_times = (
1543
+ kernel32.GetProcessTimes
1544
+ )
1545
+ get_process_times.argtypes = (
1546
+ wintypes.HANDLE,
1547
+ ctypes.POINTER(
1548
+ wintypes.FILETIME
1549
+ ),
1550
+ ctypes.POINTER(
1551
+ wintypes.FILETIME
1552
+ ),
1553
+ ctypes.POINTER(
1554
+ wintypes.FILETIME
1555
+ ),
1556
+ ctypes.POINTER(
1557
+ wintypes.FILETIME
1558
+ ),
1559
+ )
1560
+ get_process_times.restype = (
1561
+ wintypes.BOOL
1562
+ )
1563
+
1564
+ close_handle = kernel32.CloseHandle
1565
+ close_handle.argtypes = (
1566
+ wintypes.HANDLE,
1567
+ )
1568
+ close_handle.restype = (
1569
+ wintypes.BOOL
1570
+ )
1571
+
1572
+ access = (
1573
+ PROCESS_QUERY_LIMITED_INFORMATION
1574
+ | SYNCHRONIZE
1575
+ )
1576
+
1577
+ handle = open_process(
1578
+ access,
1579
+ False,
1580
+ pid,
1581
+ )
1582
+
1583
+ if not handle:
1584
+ error = ctypes.get_last_error()
1585
+
1586
+ if error == ERROR_INVALID_PARAMETER:
1587
+ return {
1588
+ "status": "missing",
1589
+ "identity": None,
1590
+ }
1591
+
1592
+ # Access denied or another unknown condition must
1593
+ # never be interpreted as proof that the process died.
1594
+ return {
1595
+ "status": "indeterminate",
1596
+ "identity": None,
1597
+ }
1598
+
1599
+ try:
1600
+ wait_result = (
1601
+ wait_for_single_object(
1602
+ handle,
1603
+ 0,
1604
+ )
1605
+ )
1606
+
1607
+ if wait_result == WAIT_OBJECT_0:
1608
+ return {
1609
+ "status": "missing",
1610
+ "identity": None,
1611
+ }
1612
+
1613
+ if wait_result == WAIT_FAILED:
1614
+ return {
1615
+ "status": "indeterminate",
1616
+ "identity": None,
1617
+ }
1618
+
1619
+ if wait_result != WAIT_TIMEOUT:
1620
+ return {
1621
+ "status": "indeterminate",
1622
+ "identity": None,
1623
+ }
1624
+
1625
+ creation = wintypes.FILETIME()
1626
+ exit_time = wintypes.FILETIME()
1627
+ kernel_time = wintypes.FILETIME()
1628
+ user_time = wintypes.FILETIME()
1629
+
1630
+ ok = get_process_times(
1631
+ handle,
1632
+ ctypes.byref(
1633
+ creation
1634
+ ),
1635
+ ctypes.byref(
1636
+ exit_time
1637
+ ),
1638
+ ctypes.byref(
1639
+ kernel_time
1640
+ ),
1641
+ ctypes.byref(
1642
+ user_time
1643
+ ),
1644
+ )
1645
+
1646
+ if not ok:
1647
+ return {
1648
+ "status": "indeterminate",
1649
+ "identity": None,
1650
+ }
1651
+
1652
+ creation_ticks = (
1653
+ (
1654
+ int(
1655
+ creation.dwHighDateTime
1656
+ )
1657
+ << 32
1658
+ )
1659
+ | int(
1660
+ creation.dwLowDateTime
1661
+ )
1662
+ )
1663
+
1664
+ if creation_ticks <= 0:
1665
+ return {
1666
+ "status": "indeterminate",
1667
+ "identity": None,
1668
+ }
1669
+
1670
+ return {
1671
+ "status": "alive",
1672
+ "identity": (
1673
+ f"windows:{creation_ticks}"
1674
+ ),
1675
+ }
1676
+
1677
+ finally:
1678
+ close_handle(
1679
+ handle
1680
+ )
1681
+
1682
+ # ---------------------------------------------------------
1683
+ # Linux
1684
+ # ---------------------------------------------------------
1685
+
1686
+ if sys.platform.startswith(
1687
+ "linux"
1688
+ ):
1689
+ stat_path = Path(
1690
+ f"/proc/{pid}/stat"
1691
+ )
1692
+
1693
+ try:
1694
+ raw = stat_path.read_text(
1695
+ encoding="utf-8",
1696
+ errors="strict",
1697
+ )
1698
+
1699
+ except FileNotFoundError:
1700
+ return {
1701
+ "status": "missing",
1702
+ "identity": None,
1703
+ }
1704
+
1705
+ except OSError:
1706
+ return {
1707
+ "status": "indeterminate",
1708
+ "identity": None,
1709
+ }
1710
+
1711
+ # Field 2 (comm) is parenthesized and may contain spaces.
1712
+ # Parse from the final ')' so the remaining sequence starts
1713
+ # at field 3 (state).
1714
+ close_paren = raw.rfind(
1715
+ ")"
1716
+ )
1717
+
1718
+ if close_paren == -1:
1719
+ return {
1720
+ "status": "indeterminate",
1721
+ "identity": None,
1722
+ }
1723
+
1724
+ remaining = raw[
1725
+ close_paren + 1:
1726
+ ].strip().split()
1727
+
1728
+ # remaining[0] == field 3.
1729
+ # starttime is field 22 => index 19 here.
1730
+ if len(remaining) <= 19:
1731
+ return {
1732
+ "status": "indeterminate",
1733
+ "identity": None,
1734
+ }
1735
+
1736
+ starttime = remaining[
1737
+ 19
1738
+ ]
1739
+
1740
+ if not starttime.isdigit():
1741
+ return {
1742
+ "status": "indeterminate",
1743
+ "identity": None,
1744
+ }
1745
+
1746
+ return {
1747
+ "status": "alive",
1748
+ "identity": (
1749
+ f"linux:{starttime}"
1750
+ ),
1751
+ }
1752
+
1753
+ # Unsupported platforms remain fail-closed.
1754
+ return {
1755
+ "status": "indeterminate",
1756
+ "identity": None,
1757
+ }
1758
+
1759
+
1760
+ def _classify_payment_intent_claim_recovery(
1761
+ claim: Mapping[str, Any],
1762
+ *,
1763
+ process_probe: Mapping[str, Any],
1764
+ ) -> dict[str, Any]:
1765
+ """Classify one persisted payment-intent claim without mutation.
1766
+
1767
+ A claim is reclaimable only when process evidence positively proves
1768
+ that the original claiming process is gone or that its PID has been
1769
+ reused by a different process instance.
1770
+
1771
+ Any malformed or incomplete evidence fails closed.
1772
+ """
1773
+
1774
+ if not isinstance(
1775
+ claim,
1776
+ Mapping,
1777
+ ):
1778
+ return {
1779
+ "status": "indeterminate",
1780
+ "safe_to_reclaim": False,
1781
+ }
1782
+
1783
+ if claim.get(
1784
+ "format"
1785
+ ) != "qruntime-x402-intent-claim-v2":
1786
+ return {
1787
+ "status": "indeterminate",
1788
+ "safe_to_reclaim": False,
1789
+ }
1790
+
1791
+ pid = claim.get(
1792
+ "pid"
1793
+ )
1794
+
1795
+ claimed_identity = claim.get(
1796
+ "process_identity"
1797
+ )
1798
+
1799
+ claimed_identity_status = claim.get(
1800
+ "process_identity_status"
1801
+ )
1802
+
1803
+ if (
1804
+ isinstance(pid, bool)
1805
+ or not isinstance(pid, int)
1806
+ or pid <= 0
1807
+ or not isinstance(
1808
+ claimed_identity,
1809
+ str,
1810
+ )
1811
+ or not claimed_identity
1812
+ or claimed_identity_status != "alive"
1813
+ ):
1814
+ return {
1815
+ "status": "indeterminate",
1816
+ "safe_to_reclaim": False,
1817
+ }
1818
+
1819
+ if not isinstance(
1820
+ process_probe,
1821
+ Mapping,
1822
+ ):
1823
+ return {
1824
+ "status": "indeterminate",
1825
+ "safe_to_reclaim": False,
1826
+ }
1827
+
1828
+ probe_status = process_probe.get(
1829
+ "status"
1830
+ )
1831
+
1832
+ probe_identity = process_probe.get(
1833
+ "identity"
1834
+ )
1835
+
1836
+ if probe_status == "missing":
1837
+ return {
1838
+ "status": "stale_missing",
1839
+ "safe_to_reclaim": True,
1840
+ }
1841
+
1842
+ if probe_status != "alive":
1843
+ return {
1844
+ "status": "indeterminate",
1845
+ "safe_to_reclaim": False,
1846
+ }
1847
+
1848
+ if (
1849
+ not isinstance(
1850
+ probe_identity,
1851
+ str,
1852
+ )
1853
+ or not probe_identity
1854
+ ):
1855
+ return {
1856
+ "status": "indeterminate",
1857
+ "safe_to_reclaim": False,
1858
+ }
1859
+
1860
+ if probe_identity == claimed_identity:
1861
+ return {
1862
+ "status": "active",
1863
+ "safe_to_reclaim": False,
1864
+ }
1865
+
1866
+ return {
1867
+ "status": "stale_pid_reused",
1868
+ "safe_to_reclaim": True,
1869
+ }
1870
+
1871
+
1872
+ def _preview_payment_intent_claim_recovery(
1873
+ logical_intent_id: str,
1874
+ *,
1875
+ probe_process_identity=None,
1876
+ ) -> dict[str, Any]:
1877
+ """Read and classify one persisted intent claim without mutation."""
1878
+
1879
+ if probe_process_identity is None:
1880
+ probe_process_identity = _probe_process_identity
1881
+
1882
+ claim_path = _intent_claim_path(
1883
+ logical_intent_id
1884
+ )
1885
+
1886
+ try:
1887
+ raw = claim_path.read_bytes()
1888
+ except OSError as exc:
1889
+ raise SettlementStoreError(
1890
+ "payment intent claim could not be read"
1891
+ ) from exc
1892
+
1893
+ try:
1894
+ claim = json.loads(raw)
1895
+ except (
1896
+ json.JSONDecodeError,
1897
+ UnicodeDecodeError,
1898
+ ) as exc:
1899
+ raise SettlementStoreError(
1900
+ "payment intent claim contains invalid JSON"
1901
+ ) from exc
1902
+
1903
+ if not isinstance(claim, Mapping):
1904
+ raise SettlementStoreError(
1905
+ "payment intent claim must contain a JSON object"
1906
+ )
1907
+
1908
+ persisted_intent_id = claim.get(
1909
+ "intent_id"
1910
+ )
1911
+
1912
+ if (
1913
+ not isinstance(
1914
+ persisted_intent_id,
1915
+ str,
1916
+ )
1917
+ or persisted_intent_id != logical_intent_id
1918
+ ):
1919
+ raise SettlementStoreError(
1920
+ "payment intent claim identity does not match requested intent"
1921
+ )
1922
+
1923
+ pid = claim.get(
1924
+ "pid"
1925
+ )
1926
+
1927
+ if (
1928
+ isinstance(pid, bool)
1929
+ or not isinstance(pid, int)
1930
+ or pid <= 0
1931
+ ):
1932
+ process_probe = {
1933
+ "status": "indeterminate",
1934
+ "identity": None,
1935
+ }
1936
+ else:
1937
+ try:
1938
+ process_probe = (
1939
+ probe_process_identity(pid)
1940
+ )
1941
+ except Exception:
1942
+ process_probe = {
1943
+ "status": "indeterminate",
1944
+ "identity": None,
1945
+ }
1946
+
1947
+ if not isinstance(
1948
+ process_probe,
1949
+ Mapping,
1950
+ ):
1951
+ process_probe = {
1952
+ "status": "indeterminate",
1953
+ "identity": None,
1954
+ }
1955
+
1956
+ decision = (
1957
+ _classify_payment_intent_claim_recovery(
1958
+ claim,
1959
+ process_probe=process_probe,
1960
+ )
1961
+ )
1962
+
1963
+ return {
1964
+ "intent_id": logical_intent_id,
1965
+ "status": decision["status"],
1966
+ "safe_to_reclaim": decision[
1967
+ "safe_to_reclaim"
1968
+ ],
1969
+ "pid": pid,
1970
+ "process_identity": claim.get(
1971
+ "process_identity"
1972
+ ),
1973
+ "process_probe": dict(
1974
+ process_probe
1975
+ ),
1976
+ }
1977
+
1978
+
1979
+ def _reclaim_payment_intent_claim_if_stale_unlocked(
1980
+ logical_intent_id: str,
1981
+ *,
1982
+ probe_process_identity=None,
1983
+ ) -> bool:
1984
+ """Reclaim one positively stale claim while caller holds OS guard."""
1985
+
1986
+ if probe_process_identity is None:
1987
+ probe_process_identity = _probe_process_identity
1988
+
1989
+ claim_path = _intent_claim_path(
1990
+ logical_intent_id
1991
+ )
1992
+
1993
+ try:
1994
+ initial_raw = claim_path.read_bytes()
1995
+ except OSError as exc:
1996
+ raise SettlementStoreError(
1997
+ "payment intent claim could not be read for reclaim"
1998
+ ) from exc
1999
+
2000
+ try:
2001
+ claim = json.loads(
2002
+ initial_raw
2003
+ )
2004
+ except (
2005
+ json.JSONDecodeError,
2006
+ UnicodeDecodeError,
2007
+ ) as exc:
2008
+ raise SettlementStoreError(
2009
+ "payment intent claim contains invalid JSON"
2010
+ ) from exc
2011
+
2012
+ if not isinstance(
2013
+ claim,
2014
+ Mapping,
2015
+ ):
2016
+ raise SettlementStoreError(
2017
+ "payment intent claim must contain a JSON object"
2018
+ )
2019
+
2020
+ persisted_intent_id = claim.get(
2021
+ "intent_id"
2022
+ )
2023
+
2024
+ if (
2025
+ not isinstance(
2026
+ persisted_intent_id,
2027
+ str,
2028
+ )
2029
+ or persisted_intent_id
2030
+ != logical_intent_id
2031
+ ):
2032
+ raise SettlementStoreError(
2033
+ "payment intent claim identity does not match requested intent"
2034
+ )
2035
+
2036
+ pid = claim.get(
2037
+ "pid"
2038
+ )
2039
+
2040
+ if (
2041
+ isinstance(pid, bool)
2042
+ or not isinstance(pid, int)
2043
+ or pid <= 0
2044
+ ):
2045
+ process_probe = {
2046
+ "status": "indeterminate",
2047
+ "identity": None,
2048
+ }
2049
+ else:
2050
+ try:
2051
+ process_probe = probe_process_identity(
2052
+ pid
2053
+ )
2054
+ except Exception:
2055
+ process_probe = {
2056
+ "status": "indeterminate",
2057
+ "identity": None,
2058
+ }
2059
+
2060
+ if not isinstance(
2061
+ process_probe,
2062
+ Mapping,
2063
+ ):
2064
+ process_probe = {
2065
+ "status": "indeterminate",
2066
+ "identity": None,
2067
+ }
2068
+
2069
+ decision = (
2070
+ _classify_payment_intent_claim_recovery(
2071
+ claim,
2072
+ process_probe=process_probe,
2073
+ )
2074
+ )
2075
+
2076
+ if not decision["safe_to_reclaim"]:
2077
+ return False
2078
+
2079
+ try:
2080
+ current_raw = claim_path.read_bytes()
2081
+ except OSError:
2082
+ return False
2083
+
2084
+ if current_raw != initial_raw:
2085
+ return False
2086
+
2087
+ try:
2088
+ claim_path.unlink()
2089
+ except FileNotFoundError:
2090
+ return False
2091
+ except OSError as exc:
2092
+ raise SettlementStoreError(
2093
+ "payment intent claim could not be reclaimed"
2094
+ ) from exc
2095
+
2096
+ return True
2097
+
2098
+
2099
+ def _reclaim_payment_intent_claim_if_stale(
2100
+ logical_intent_id: str,
2101
+ *,
2102
+ probe_process_identity=None,
2103
+ ) -> bool:
2104
+ """Reclaim one stale claim under its OS guard."""
2105
+
2106
+ guard = _acquire_payment_intent_claim_guard(
2107
+ logical_intent_id
2108
+ )
2109
+
2110
+ try:
2111
+ return _reclaim_payment_intent_claim_if_stale_unlocked(
2112
+ logical_intent_id,
2113
+ probe_process_identity=probe_process_identity,
2114
+ )
2115
+ finally:
2116
+ _release_payment_intent_claim_guard(
2117
+ guard
2118
+ )
2119
+
2120
+
2121
+
2122
+
2123
+
2124
+
2125
+ def _intent_claim_guard_path(
2126
+ logical_intent_id: str,
2127
+ ) -> Path:
2128
+ return (
2129
+ _intent_claim_directory()
2130
+ / f"{_persistent_intent_key(logical_intent_id)}.guard"
2131
+ )
2132
+
2133
+
2134
+ def _acquire_payment_intent_claim_guard(
2135
+ logical_intent_id: str,
2136
+ ) -> dict[str, Any]:
2137
+ """Acquire a non-blocking OS-level guard for one payment intent."""
2138
+
2139
+ import errno
2140
+
2141
+ guard_path = _intent_claim_guard_path(
2142
+ logical_intent_id
2143
+ )
2144
+
2145
+ guard_path.parent.mkdir(
2146
+ parents=True,
2147
+ exist_ok=True,
2148
+ )
2149
+
2150
+ try:
2151
+ handle = open(
2152
+ guard_path,
2153
+ "a+b",
2154
+ buffering=0,
2155
+ )
2156
+ except OSError as exc:
2157
+ raise SettlementStoreError(
2158
+ "payment intent claim guard could not be opened"
2159
+ ) from exc
2160
+
2161
+ try:
2162
+ if os.name == "nt":
2163
+ import msvcrt
2164
+
2165
+ handle.seek(0)
2166
+
2167
+ try:
2168
+ msvcrt.locking(
2169
+ handle.fileno(),
2170
+ msvcrt.LK_NBLCK,
2171
+ 1,
2172
+ )
2173
+ except OSError as exc:
2174
+ handle.close()
2175
+
2176
+ if exc.errno in (
2177
+ errno.EACCES,
2178
+ errno.EAGAIN,
2179
+ ):
2180
+ raise IntentInFlightError(
2181
+ "payment intent guard is already held"
2182
+ ) from exc
2183
+
2184
+ raise SettlementStoreError(
2185
+ "payment intent claim guard lock failed"
2186
+ ) from exc
2187
+
2188
+ return {
2189
+ "backend": "msvcrt",
2190
+ "handle": handle,
2191
+ "path": guard_path,
2192
+ }
2193
+
2194
+ try:
2195
+ import fcntl
2196
+ except ImportError as exc:
2197
+ handle.close()
2198
+
2199
+ raise SettlementStoreError(
2200
+ "payment intent claim guard is unsupported "
2201
+ "on this platform"
2202
+ ) from exc
2203
+
2204
+ try:
2205
+ fcntl.flock(
2206
+ handle.fileno(),
2207
+ fcntl.LOCK_EX
2208
+ | fcntl.LOCK_NB,
2209
+ )
2210
+ except OSError as exc:
2211
+ handle.close()
2212
+
2213
+ if exc.errno in (
2214
+ errno.EACCES,
2215
+ errno.EAGAIN,
2216
+ ):
2217
+ raise IntentInFlightError(
2218
+ "payment intent guard is already held"
2219
+ ) from exc
2220
+
2221
+ raise SettlementStoreError(
2222
+ "payment intent claim guard lock failed"
2223
+ ) from exc
2224
+
2225
+ return {
2226
+ "backend": "fcntl",
2227
+ "handle": handle,
2228
+ "path": guard_path,
2229
+ }
2230
+
2231
+ except Exception:
2232
+ if not handle.closed:
2233
+ handle.close()
2234
+ raise
2235
+
2236
+
2237
+ def _release_payment_intent_claim_guard(
2238
+ guard: Mapping[str, Any],
2239
+ ) -> None:
2240
+ """Release an OS-level payment-intent guard."""
2241
+
2242
+ handle = guard.get(
2243
+ "handle"
2244
+ )
2245
+
2246
+ backend = guard.get(
2247
+ "backend"
2248
+ )
2249
+
2250
+ if handle is None:
2251
+ raise SettlementStoreError(
2252
+ "payment intent claim guard handle is missing"
2253
+ )
2254
+
2255
+ if handle.closed:
2256
+ return
2257
+
2258
+ try:
2259
+ if backend == "msvcrt":
2260
+ import msvcrt
2261
+
2262
+ handle.seek(0)
2263
+
2264
+ msvcrt.locking(
2265
+ handle.fileno(),
2266
+ msvcrt.LK_UNLCK,
2267
+ 1,
2268
+ )
2269
+
2270
+ elif backend == "fcntl":
2271
+ import fcntl
2272
+
2273
+ fcntl.flock(
2274
+ handle.fileno(),
2275
+ fcntl.LOCK_UN,
2276
+ )
2277
+
2278
+ else:
2279
+ raise SettlementStoreError(
2280
+ "payment intent claim guard backend is invalid"
2281
+ )
2282
+
2283
+ finally:
2284
+ handle.close()
2285
+
2286
+
2287
+ def _intent_claim_path(logical_intent_id: str) -> Path:
2288
+ return (
2289
+ _intent_claim_directory()
2290
+ / f"{_persistent_intent_key(logical_intent_id)}.claim"
2291
+ )
2292
+
2293
+
2294
+ def _acquire_payment_intent_claim_unlocked(
2295
+ logical_intent_id: str,
2296
+ resource: str,
2297
+ ) -> Path:
2298
+ """Atomically claim one logical payment intent across processes."""
2299
+ claims = _intent_claim_directory()
2300
+ claims.mkdir(parents=True, exist_ok=True)
2301
+
2302
+ path = _intent_claim_path(logical_intent_id)
2303
+
2304
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
2305
+ flags |= getattr(os, "O_BINARY", 0)
2306
+
2307
+ try:
2308
+ fd = os.open(path, flags, 0o600)
2309
+ except FileExistsError as exc:
2310
+ raise IntentInFlightError(
2311
+ "preflight refused payment: payment intent "
2312
+ f"{logical_intent_id} is already in-flight in another process"
2313
+ ) from exc
2314
+
2315
+ try:
2316
+ pid = os.getpid()
2317
+ probe = _probe_process_identity(pid)
2318
+ status = str(probe.get("status", ""))
2319
+ identity = probe.get("identity")
2320
+ if status != "alive" or not isinstance(identity, str) or not identity:
2321
+ raise SettlementStoreError(
2322
+ "current process identity could not be established safely for intent claim"
2323
+ )
2324
+ except Exception:
2325
+ os.close(fd)
2326
+ path.unlink(missing_ok=True)
2327
+ raise
2328
+
2329
+ payload = {
2330
+ "format": "qruntime-x402-intent-claim-v2",
2331
+ "intent_id": logical_intent_id,
2332
+ "intent_key": _persistent_intent_key(logical_intent_id),
2333
+ "resource": resource,
2334
+ "pid": pid,
2335
+ "process_identity": identity,
2336
+ "process_identity_status": status,
2337
+ "claimed_at": datetime.now(timezone.utc).isoformat(),
2338
+ }
2339
+
2340
+ try:
2341
+ encoded = json.dumps(
2342
+ payload,
2343
+ indent=2,
2344
+ ensure_ascii=False,
2345
+ ).encode("utf-8")
2346
+
2347
+ os.write(fd, encoded)
2348
+ os.fsync(fd)
2349
+ except Exception:
2350
+ os.close(fd)
2351
+ path.unlink(missing_ok=True)
2352
+ raise
2353
+ else:
2354
+ os.close(fd)
2355
+
2356
+ return path
2357
+
2358
+
2359
+ def _acquire_payment_intent_claim(
2360
+ logical_intent_id: str,
2361
+ resource: str,
2362
+ ) -> Path:
2363
+ """Acquire a durable claim, recovering one proven stale claim once."""
2364
+
2365
+ guard = _acquire_payment_intent_claim_guard(
2366
+ logical_intent_id
2367
+ )
2368
+
2369
+ try:
2370
+ try:
2371
+ return _acquire_payment_intent_claim_unlocked(
2372
+ logical_intent_id,
2373
+ resource,
2374
+ )
2375
+
2376
+ except IntentInFlightError:
2377
+ reclaimed = (
2378
+ _reclaim_payment_intent_claim_if_stale_unlocked(
2379
+ logical_intent_id
2380
+ )
2381
+ )
2382
+
2383
+ if not reclaimed:
2384
+ raise
2385
+
2386
+ # Retry exactly once while still holding the same guard.
2387
+ # If another non-cooperating writer created a claim anyway,
2388
+ # O_EXCL fails closed again.
2389
+ return _acquire_payment_intent_claim_unlocked(
2390
+ logical_intent_id,
2391
+ resource,
2392
+ )
2393
+
2394
+ finally:
2395
+ _release_payment_intent_claim_guard(
2396
+ guard
2397
+ )
2398
+
2399
+
2400
+
2401
+
2402
+
2403
+
2404
+ def _release_payment_intent_claim(path: Path) -> None:
2405
+ path.unlink(missing_ok=True)
2406
+
2407
+
2408
+ def _reserve_aggregate_budget(
2409
+ logical_intent_id: str,
2410
+ resource: str,
2411
+ requirement: Any,
2412
+ budget: Any,
2413
+ *,
2414
+ agent_id: str | None = None,
2415
+ session_id: str | None = None,
2416
+ ):
2417
+ """Atomically check and reserve aggregate budget.
2418
+
2419
+ The budget decision and reservation write happen
2420
+ under the same persistent-store transaction lock.
2421
+ """
2422
+ from qruntime.budget import (
2423
+ BudgetLimit,
2424
+ evaluate_budget,
2425
+ )
2426
+ from qruntime.spend import (
2427
+ _normalize_agent_id,
2428
+ _normalize_session_id,
2429
+ summarize_spend_entries,
2430
+ )
2431
+
2432
+ if not isinstance(
2433
+ budget,
2434
+ BudgetLimit,
2435
+ ):
2436
+ raise TypeError(
2437
+ "budget must be a BudgetLimit"
2438
+ )
2439
+
2440
+ if (
2441
+ budget.network
2442
+ != requirement.network
2443
+ or budget.asset
2444
+ != requirement.asset
2445
+ ):
2446
+ raise ValueError(
2447
+ "budget dimensions do not match "
2448
+ "selected payment requirement"
2449
+ )
2450
+
2451
+ if budget.origin is not None:
2452
+ from qruntime.policy import (
2453
+ _normalize_origin,
2454
+ )
2455
+
2456
+ resource_origin = (
2457
+ _normalize_origin(resource)
2458
+ )
2459
+
2460
+ if resource_origin != budget.origin:
2461
+ raise ValueError(
2462
+ "budget origin does not match "
2463
+ "selected payment resource"
2464
+ )
2465
+
2466
+ payment_agent_id = None
2467
+
2468
+ if agent_id is not None:
2469
+ payment_agent_id = (
2470
+ _normalize_agent_id(
2471
+ agent_id
2472
+ )
2473
+ )
2474
+
2475
+ if budget.agent_id is not None:
2476
+ if payment_agent_id is None:
2477
+ raise ValueError(
2478
+ "agent-scoped budget requires agent_id"
2479
+ )
2480
+
2481
+ if payment_agent_id != budget.agent_id:
2482
+ raise ValueError(
2483
+ "budget agent does not match "
2484
+ "payment agent"
2485
+ )
2486
+
2487
+ payment_session_id = None
2488
+
2489
+ if session_id is not None:
2490
+ payment_session_id = (
2491
+ _normalize_session_id(
2492
+ session_id
2493
+ )
2494
+ )
2495
+
2496
+ if budget.session_id is not None:
2497
+ if payment_session_id is None:
2498
+ raise ValueError(
2499
+ "session-scoped budget requires session_id"
2500
+ )
2501
+
2502
+ if (
2503
+ payment_session_id
2504
+ != budget.session_id
2505
+ ):
2506
+ raise ValueError(
2507
+ "budget session does not match "
2508
+ "payment session"
2509
+ )
2510
+
2511
+ intent_key = _persistent_intent_key(
2512
+ logical_intent_id
2513
+ )
2514
+
2515
+ fingerprint = (
2516
+ requirement.fingerprint()
2517
+ )
2518
+
2519
+ now = datetime.now(
2520
+ timezone.utc
2521
+ ).isoformat()
2522
+
2523
+ reservation = {
2524
+ "intent_id": logical_intent_id,
2525
+ "intent_key": intent_key,
2526
+ "resource": resource,
2527
+ "agent_id": payment_agent_id,
2528
+ "session_id": payment_session_id,
2529
+ "quote_fingerprint": fingerprint,
2530
+ "requirement": (
2531
+ requirement.canonical_dict()
2532
+ ),
2533
+ "transaction": None,
2534
+ "network": requirement.network,
2535
+ "state": "reserved",
2536
+ "first_seen_at": now,
2537
+ "updated_at": now,
2538
+ "reservation": {
2539
+ "reserved_at": now,
2540
+ "budget_limit_atomic": (
2541
+ budget.max_amount_atomic
2542
+ ),
2543
+ "network": budget.network,
2544
+ "asset": budget.asset,
2545
+ "origin": budget.origin,
2546
+ "agent_id": budget.agent_id,
2547
+ "session_id": budget.session_id,
2548
+ },
2549
+ }
2550
+
2551
+ def mutate(
2552
+ store: dict[str, Any],
2553
+ ):
2554
+ existing = store[
2555
+ "entries"
2556
+ ].get(intent_key)
2557
+
2558
+ if existing is not None:
2559
+ raise SettlementStoreError(
2560
+ "persistent economic state "
2561
+ "already exists for payment intent"
2562
+ )
2563
+
2564
+ try:
2565
+ summary = (
2566
+ summarize_spend_entries(
2567
+ (
2568
+ entry
2569
+ for entry in (
2570
+ store[
2571
+ "entries"
2572
+ ].values()
2573
+ )
2574
+ if isinstance(
2575
+ entry,
2576
+ Mapping,
2577
+ )
2578
+ ),
2579
+ origin=budget.origin,
2580
+ agent_id=budget.agent_id,
2581
+ session_id=budget.session_id,
2582
+ )
2583
+ )
2584
+ except ValueError as exc:
2585
+ raise SettlementStoreError(
2586
+ "persistent economic state "
2587
+ "is invalid; refusing budget "
2588
+ "reservation fail-closed"
2589
+ ) from exc
2590
+
2591
+ decision = evaluate_budget(
2592
+ budget,
2593
+ summary=summary,
2594
+ requested_amount_atomic=(
2595
+ requirement.amount_atomic
2596
+ ),
2597
+ )
2598
+
2599
+ if not decision.allowed:
2600
+ return decision, False
2601
+
2602
+ store[
2603
+ "entries"
2604
+ ][intent_key] = reservation
2605
+
2606
+ return decision, True
2607
+
2608
+ return _mutate_pending_store(
2609
+ mutate
2610
+ )
2611
+
2612
+
2613
+ def _reserve_budget_set(
2614
+ logical_intent_id: str,
2615
+ resource: str,
2616
+ requirement: Any,
2617
+ budget_set: Any,
2618
+ *,
2619
+ agent_id: str | None = None,
2620
+ session_id: str | None = None,
2621
+ ):
2622
+ """Atomically evaluate all budget constraints and reserve once."""
2623
+ from qruntime.budget import (
2624
+ BudgetSet,
2625
+ BudgetSetDecision,
2626
+ evaluate_budget,
2627
+ )
2628
+ from qruntime.policy import (
2629
+ _normalize_origin,
2630
+ )
2631
+ from qruntime.spend import (
2632
+ _normalize_agent_id,
2633
+ _normalize_session_id,
2634
+ summarize_spend_entries,
2635
+ )
2636
+
2637
+ if not isinstance(
2638
+ budget_set,
2639
+ BudgetSet,
2640
+ ):
2641
+ raise TypeError(
2642
+ "budget_set must be a BudgetSet"
2643
+ )
2644
+
2645
+ resource_origin = (
2646
+ _normalize_origin(resource)
2647
+ )
2648
+
2649
+ payment_agent_id = None
2650
+
2651
+ if agent_id is not None:
2652
+ payment_agent_id = (
2653
+ _normalize_agent_id(
2654
+ agent_id
2655
+ )
2656
+ )
2657
+
2658
+ payment_session_id = None
2659
+
2660
+ if session_id is not None:
2661
+ payment_session_id = (
2662
+ _normalize_session_id(
2663
+ session_id
2664
+ )
2665
+ )
2666
+
2667
+ for limit in budget_set.limits:
2668
+ if (
2669
+ limit.network
2670
+ != requirement.network
2671
+ or limit.asset
2672
+ != requirement.asset
2673
+ ):
2674
+ raise ValueError(
2675
+ "budget dimensions do not match "
2676
+ "selected payment requirement"
2677
+ )
2678
+
2679
+ if (
2680
+ limit.origin is not None
2681
+ and limit.origin
2682
+ != resource_origin
2683
+ ):
2684
+ raise ValueError(
2685
+ "budget origin does not match "
2686
+ "selected payment resource"
2687
+ )
2688
+
2689
+ if limit.agent_id is not None:
2690
+ if payment_agent_id is None:
2691
+ raise ValueError(
2692
+ "agent-scoped budget requires agent_id"
2693
+ )
2694
+
2695
+ if (
2696
+ payment_agent_id
2697
+ != limit.agent_id
2698
+ ):
2699
+ raise ValueError(
2700
+ "budget agent does not match "
2701
+ "payment agent"
2702
+ )
2703
+
2704
+ if limit.session_id is not None:
2705
+ if payment_session_id is None:
2706
+ raise ValueError(
2707
+ "session-scoped budget "
2708
+ "requires session_id"
2709
+ )
2710
+
2711
+ if (
2712
+ payment_session_id
2713
+ != limit.session_id
2714
+ ):
2715
+ raise ValueError(
2716
+ "budget session does not match "
2717
+ "payment session"
2718
+ )
2719
+
2720
+ intent_key = _persistent_intent_key(
2721
+ logical_intent_id
2722
+ )
2723
+
2724
+ now = datetime.now(
2725
+ timezone.utc
2726
+ ).isoformat()
2727
+
2728
+ reservation = {
2729
+ "intent_id": logical_intent_id,
2730
+ "intent_key": intent_key,
2731
+ "resource": resource,
2732
+ "agent_id": payment_agent_id,
2733
+ "session_id": payment_session_id,
2734
+ "quote_fingerprint": (
2735
+ requirement.fingerprint()
2736
+ ),
2737
+ "requirement": (
2738
+ requirement.canonical_dict()
2739
+ ),
2740
+ "transaction": None,
2741
+ "network": requirement.network,
2742
+ "state": "reserved",
2743
+ "first_seen_at": now,
2744
+ "updated_at": now,
2745
+ "reservation": {
2746
+ "reserved_at": now,
2747
+ "constraints": [
2748
+ {
2749
+ "network": limit.network,
2750
+ "asset": limit.asset,
2751
+ "origin": limit.origin,
2752
+ "agent_id": limit.agent_id,
2753
+ "session_id": limit.session_id,
2754
+ "budget_limit_atomic": (
2755
+ limit.max_amount_atomic
2756
+ ),
2757
+ }
2758
+ for limit in budget_set.limits
2759
+ ],
2760
+ },
2761
+ }
2762
+
2763
+ def mutate(
2764
+ store: dict[str, Any],
2765
+ ):
2766
+ existing = store[
2767
+ "entries"
2768
+ ].get(intent_key)
2769
+
2770
+ if existing is not None:
2771
+ raise SettlementStoreError(
2772
+ "persistent economic state "
2773
+ "already exists for payment intent"
2774
+ )
2775
+
2776
+ entries = tuple(
2777
+ entry
2778
+ for entry in (
2779
+ store[
2780
+ "entries"
2781
+ ].values()
2782
+ )
2783
+ if isinstance(
2784
+ entry,
2785
+ Mapping,
2786
+ )
2787
+ )
2788
+
2789
+ decisions = []
2790
+
2791
+ try:
2792
+ for limit in budget_set.limits:
2793
+ summary = (
2794
+ summarize_spend_entries(
2795
+ entries,
2796
+ origin=limit.origin,
2797
+ agent_id=limit.agent_id,
2798
+ session_id=limit.session_id,
2799
+ )
2800
+ )
2801
+
2802
+ decision = evaluate_budget(
2803
+ limit,
2804
+ summary=summary,
2805
+ requested_amount_atomic=(
2806
+ requirement.amount_atomic
2807
+ ),
2808
+ )
2809
+
2810
+ decisions.append(
2811
+ decision
2812
+ )
2813
+
2814
+ except ValueError as exc:
2815
+ raise SettlementStoreError(
2816
+ "persistent economic state "
2817
+ "is invalid; refusing budget "
2818
+ "reservation fail-closed"
2819
+ ) from exc
2820
+
2821
+ result = BudgetSetDecision(
2822
+ allowed=all(
2823
+ decision.allowed
2824
+ for decision in decisions
2825
+ ),
2826
+ decisions=tuple(
2827
+ decisions
2828
+ ),
2829
+ )
2830
+
2831
+ if not result.allowed:
2832
+ return result, False
2833
+
2834
+ store[
2835
+ "entries"
2836
+ ][intent_key] = reservation
2837
+
2838
+ return result, True
2839
+
2840
+ return _mutate_pending_store(
2841
+ mutate
2842
+ )
2843
+
2844
+
2845
+ def _mark_payment_submission_unknown(
2846
+ logical_intent_id: str,
2847
+ resource: str | None = None,
2848
+ requirement: Any | None = None,
2849
+ *,
2850
+ agent_id: str | None = None,
2851
+ session_id: str | None = None,
2852
+ ) -> bool:
2853
+ """Durably mark an intent before the potentially economic handoff.
2854
+
2855
+ Existing reserved state is advanced to submission_unknown.
2856
+ Without a budget reservation, a new durable economic intent entry
2857
+ is created so a crash cannot leave only an orphaned claim file.
2858
+ """
2859
+ intent_key = _persistent_intent_key(
2860
+ logical_intent_id
2861
+ )
2862
+
2863
+ normalized_agent_id = None
2864
+ if agent_id is not None:
2865
+ from qruntime.spend import _normalize_agent_id
2866
+
2867
+ normalized_agent_id = _normalize_agent_id(
2868
+ agent_id
2869
+ )
2870
+
2871
+ normalized_session_id = None
2872
+ if session_id is not None:
2873
+ from qruntime.spend import _normalize_session_id
2874
+
2875
+ normalized_session_id = _normalize_session_id(
2876
+ session_id
2877
+ )
2878
+
2879
+ now = datetime.now(
2880
+ timezone.utc
2881
+ ).isoformat()
2882
+
2883
+ def mutate(
2884
+ store: dict[str, Any],
2885
+ ):
2886
+ raw = store[
2887
+ "entries"
2888
+ ].get(intent_key)
2889
+
2890
+ if isinstance(
2891
+ raw,
2892
+ Mapping,
2893
+ ):
2894
+ state = str(
2895
+ raw.get("state", "")
2896
+ )
2897
+
2898
+ if state == "submission_unknown":
2899
+ return True, False
2900
+
2901
+ if state != "reserved":
2902
+ return False, False
2903
+
2904
+ entry = dict(raw)
2905
+
2906
+ entry["state"] = "submission_unknown"
2907
+ entry["submission_unknown_since"] = now
2908
+ entry["updated_at"] = now
2909
+
2910
+ store[
2911
+ "entries"
2912
+ ][intent_key] = entry
2913
+
2914
+ return True, True
2915
+
2916
+ if (
2917
+ resource is None
2918
+ or requirement is None
2919
+ ):
2920
+ return False, False
2921
+
2922
+ entry = {
2923
+ "intent_id": logical_intent_id,
2924
+ "intent_key": intent_key,
2925
+ "resource": resource,
2926
+ "agent_id": normalized_agent_id,
2927
+ "session_id": normalized_session_id,
2928
+ "quote_fingerprint": (
2929
+ requirement.fingerprint()
2930
+ ),
2931
+ "requirement": (
2932
+ requirement.canonical_dict()
2933
+ ),
2934
+ "transaction": None,
2935
+ "network": requirement.network,
2936
+ "state": "submission_unknown",
2937
+ "first_seen_at": now,
2938
+ "updated_at": now,
2939
+ "submission_unknown_since": now,
2940
+ }
2941
+
2942
+ store[
2943
+ "entries"
2944
+ ][intent_key] = entry
2945
+
2946
+ return True, True
2947
+
2948
+ return _mutate_pending_store(
2949
+ mutate
2950
+ )
2951
+
2952
+
2953
+ def _classify_eip3009_authorization_recovery(
2954
+ authorization: Mapping[str, Any],
2955
+ *,
2956
+ now_unix: int,
2957
+ authorization_state: bool | None,
2958
+ event_kind: str | None,
2959
+ ) -> dict[str, Any]:
2960
+ """Classify persisted EIP-3009 recovery evidence conservatively."""
2961
+
2962
+ if event_kind not in {
2963
+ None,
2964
+ "used",
2965
+ "canceled",
2966
+ }:
2967
+ raise ValueError(
2968
+ "event_kind must be None, 'used', or 'canceled'"
2969
+ )
2970
+
2971
+ try:
2972
+ valid_after = int(
2973
+ authorization["validAfter"]
2974
+ )
2975
+ valid_before = int(
2976
+ authorization["validBefore"]
2977
+ )
2978
+ except (
2979
+ KeyError,
2980
+ TypeError,
2981
+ ValueError,
2982
+ ):
2983
+ return {
2984
+ "status": "indeterminate",
2985
+ "safe_to_release": False,
2986
+ "requires_reconciliation": True,
2987
+ }
2988
+
2989
+ if valid_before <= valid_after:
2990
+ return {
2991
+ "status": "indeterminate",
2992
+ "safe_to_release": False,
2993
+ "requires_reconciliation": True,
2994
+ }
2995
+
2996
+ # Event evidence cannot coherently coexist with
2997
+ # authorization_state=False.
2998
+ if (
2999
+ event_kind is not None
3000
+ and authorization_state is False
3001
+ ):
3002
+ return {
3003
+ "status": "indeterminate",
3004
+ "safe_to_release": False,
3005
+ "requires_reconciliation": True,
3006
+ }
3007
+
3008
+ if event_kind == "used":
3009
+ return {
3010
+ "status": "used",
3011
+ "safe_to_release": False,
3012
+ "requires_reconciliation": True,
3013
+ }
3014
+
3015
+ if event_kind == "canceled":
3016
+ return {
3017
+ "status": "canceled",
3018
+ "safe_to_release": True,
3019
+ "requires_reconciliation": False,
3020
+ }
3021
+
3022
+ if authorization_state is True:
3023
+ return {
3024
+ "status": "consumed_indeterminate",
3025
+ "safe_to_release": False,
3026
+ "requires_reconciliation": True,
3027
+ }
3028
+
3029
+ if authorization_state is None:
3030
+ return {
3031
+ "status": "indeterminate",
3032
+ "safe_to_release": False,
3033
+ "requires_reconciliation": True,
3034
+ }
3035
+
3036
+ # ERC-3009 uses strict validity:
3037
+ # block time must be > validAfter and < validBefore.
3038
+ if now_unix <= valid_after:
3039
+ return {
3040
+ "status": "unused_not_yet_valid",
3041
+ "safe_to_release": False,
3042
+ "requires_reconciliation": True,
3043
+ }
3044
+
3045
+ if now_unix >= valid_before:
3046
+ return {
3047
+ "status": "expired_unused",
3048
+ "safe_to_release": True,
3049
+ "requires_reconciliation": False,
3050
+ }
3051
+
3052
+ return {
3053
+ "status": "unused_live",
3054
+ "safe_to_release": False,
3055
+ "requires_reconciliation": True,
3056
+ }
3057
+
3058
+
3059
+ def _aggregate_eip3009_recovery_decisions(
3060
+ authorizations: list[Mapping[str, Any]],
3061
+ *,
3062
+ now_unix: int,
3063
+ evidence_by_nonce: Mapping[str, Mapping[str, Any]],
3064
+ ) -> dict[str, Any]:
3065
+ """Aggregate recovery evidence across all authorizations of one intent.
3066
+
3067
+ Release is safe only when at least one authorization exists and
3068
+ every authorization is independently proven safe to release.
3069
+ """
3070
+
3071
+ if not authorizations:
3072
+ return {
3073
+ "safe_to_release": False,
3074
+ "requires_reconciliation": True,
3075
+ "decisions": [],
3076
+ }
3077
+
3078
+ decisions: list[dict[str, Any]] = []
3079
+
3080
+ for authorization in authorizations:
3081
+ nonce = str(
3082
+ authorization.get(
3083
+ "nonce",
3084
+ "",
3085
+ )
3086
+ )
3087
+
3088
+ evidence = evidence_by_nonce.get(
3089
+ nonce
3090
+ )
3091
+
3092
+ if not isinstance(
3093
+ evidence,
3094
+ Mapping,
3095
+ ):
3096
+ decision = {
3097
+ "status": "indeterminate",
3098
+ "safe_to_release": False,
3099
+ "requires_reconciliation": True,
3100
+ }
3101
+
3102
+ else:
3103
+ decision = (
3104
+ _classify_eip3009_authorization_recovery(
3105
+ authorization,
3106
+ now_unix=now_unix,
3107
+ authorization_state=(
3108
+ evidence.get(
3109
+ "authorization_state"
3110
+ )
3111
+ ),
3112
+ event_kind=(
3113
+ evidence.get(
3114
+ "event_kind"
3115
+ )
3116
+ ),
3117
+ )
3118
+ )
3119
+
3120
+ decisions.append(
3121
+ decision
3122
+ )
3123
+
3124
+ safe_to_release = all(
3125
+ decision[
3126
+ "safe_to_release"
3127
+ ]
3128
+ for decision in decisions
3129
+ )
3130
+
3131
+ requires_reconciliation = (
3132
+ not safe_to_release
3133
+ or any(
3134
+ decision[
3135
+ "requires_reconciliation"
3136
+ ]
3137
+ for decision in decisions
3138
+ )
3139
+ )
3140
+
3141
+ return {
3142
+ "safe_to_release": safe_to_release,
3143
+ "requires_reconciliation": (
3144
+ requires_reconciliation
3145
+ ),
3146
+ "decisions": decisions,
3147
+ }
3148
+
3149
+
3150
+ def _collect_eip3009_recovery_evidence(
3151
+ authorizations: list[Mapping[str, Any]],
3152
+ *,
3153
+ read_authorization_state,
3154
+ read_authorization_event,
3155
+ ) -> dict[str, dict[str, Any]]:
3156
+ """Collect chain evidence for persisted EIP-3009 authorizations.
3157
+
3158
+ Each evidence source is queried independently. Failure of either
3159
+ source becomes indeterminate evidence and never fails open.
3160
+ """
3161
+
3162
+ evidence_by_nonce: dict[
3163
+ str,
3164
+ dict[str, Any],
3165
+ ] = {}
3166
+
3167
+ for authorization in authorizations:
3168
+ network = str(
3169
+ authorization.get(
3170
+ "network",
3171
+ "",
3172
+ )
3173
+ )
3174
+
3175
+ asset = str(
3176
+ authorization.get(
3177
+ "asset",
3178
+ "",
3179
+ )
3180
+ )
3181
+
3182
+ authorizer = str(
3183
+ authorization.get(
3184
+ "from",
3185
+ "",
3186
+ )
3187
+ )
3188
+
3189
+ nonce = str(
3190
+ authorization.get(
3191
+ "nonce",
3192
+ "",
3193
+ )
3194
+ )
3195
+
3196
+ if not (
3197
+ network
3198
+ and asset
3199
+ and authorizer
3200
+ and nonce
3201
+ ):
3202
+ # Malformed persisted evidence is never interpreted
3203
+ # optimistically.
3204
+ if nonce:
3205
+ evidence_by_nonce[
3206
+ nonce
3207
+ ] = {
3208
+ "authorization_state": None,
3209
+ "event_kind": None,
3210
+ }
3211
+
3212
+ continue
3213
+
3214
+ try:
3215
+ authorization_state = (
3216
+ read_authorization_state(
3217
+ network=network,
3218
+ asset=asset,
3219
+ authorizer=authorizer,
3220
+ nonce=nonce,
3221
+ )
3222
+ )
3223
+
3224
+ if not isinstance(
3225
+ authorization_state,
3226
+ bool,
3227
+ ):
3228
+ authorization_state = None
3229
+
3230
+ except Exception:
3231
+ authorization_state = None
3232
+
3233
+ try:
3234
+ event_kind = (
3235
+ read_authorization_event(
3236
+ network=network,
3237
+ asset=asset,
3238
+ authorizer=authorizer,
3239
+ nonce=nonce,
3240
+ )
3241
+ )
3242
+
3243
+ if event_kind not in {
3244
+ None,
3245
+ "used",
3246
+ "canceled",
3247
+ }:
3248
+ event_kind = None
3249
+
3250
+ except Exception:
3251
+ event_kind = None
3252
+
3253
+ evidence_by_nonce[
3254
+ nonce
3255
+ ] = {
3256
+ "authorization_state": (
3257
+ authorization_state
3258
+ ),
3259
+ "event_kind": event_kind,
3260
+ }
3261
+
3262
+ return evidence_by_nonce
3263
+
3264
+
3265
+ def _preview_eip3009_intent_recovery(
3266
+ logical_intent_id: str,
3267
+ *,
3268
+ now_unix: int,
3269
+ read_authorization_state,
3270
+ read_authorization_event,
3271
+ ) -> dict[str, Any]:
3272
+ """Preview recovery for one persisted economic intent.
3273
+
3274
+ This operation is observational only. It reads durable state and
3275
+ external evidence but never mutates the persistent store.
3276
+ """
3277
+
3278
+ entry = get_persistent_payment_intent(
3279
+ logical_intent_id
3280
+ )
3281
+
3282
+ if not isinstance(
3283
+ entry,
3284
+ Mapping,
3285
+ ):
3286
+ raise SettlementStoreError(
3287
+ "persistent payment intent was not found"
3288
+ )
3289
+
3290
+ state = str(
3291
+ entry.get(
3292
+ "state",
3293
+ "",
3294
+ )
3295
+ )
3296
+
3297
+ history_raw = entry.get(
3298
+ "authorizations"
3299
+ )
3300
+
3301
+ authorizations: list[
3302
+ Mapping[str, Any]
3303
+ ] = []
3304
+
3305
+ if isinstance(
3306
+ history_raw,
3307
+ list,
3308
+ ):
3309
+ for item in history_raw:
3310
+ if not isinstance(
3311
+ item,
3312
+ Mapping,
3313
+ ):
3314
+ raise SettlementStoreError(
3315
+ "persistent authorization history "
3316
+ "contains invalid entry"
3317
+ )
3318
+
3319
+ authorizations.append(
3320
+ dict(item)
3321
+ )
3322
+
3323
+ else:
3324
+ # Backwards-compatible support for entries created before
3325
+ # authorization history was introduced.
3326
+ legacy = entry.get(
3327
+ "authorization"
3328
+ )
3329
+
3330
+ if isinstance(
3331
+ legacy,
3332
+ Mapping,
3333
+ ):
3334
+ authorizations.append(
3335
+ dict(legacy)
3336
+ )
3337
+
3338
+ evidence = (
3339
+ _collect_eip3009_recovery_evidence(
3340
+ authorizations,
3341
+ read_authorization_state=(
3342
+ read_authorization_state
3343
+ ),
3344
+ read_authorization_event=(
3345
+ read_authorization_event
3346
+ ),
3347
+ )
3348
+ )
3349
+
3350
+ aggregate = (
3351
+ _aggregate_eip3009_recovery_decisions(
3352
+ authorizations,
3353
+ now_unix=now_unix,
3354
+ evidence_by_nonce=evidence,
3355
+ )
3356
+ )
3357
+
3358
+ return {
3359
+ "intent_id": logical_intent_id,
3360
+ "state": state,
3361
+ "authorization_count": len(
3362
+ authorizations
3363
+ ),
3364
+ "safe_to_release": aggregate[
3365
+ "safe_to_release"
3366
+ ],
3367
+ "requires_reconciliation": aggregate[
3368
+ "requires_reconciliation"
3369
+ ],
3370
+ "decisions": aggregate[
3371
+ "decisions"
3372
+ ],
3373
+ "evidence_by_nonce": evidence,
3374
+ }
3375
+
3376
+
3377
+ def _resolve_eip3009_intent_if_safe(
3378
+ logical_intent_id: str,
3379
+ *,
3380
+ now_unix: int,
3381
+ read_authorization_state,
3382
+ read_authorization_event,
3383
+ ) -> bool:
3384
+ """Release ambiguous economic exposure only after proven-safe recovery.
3385
+
3386
+ The operation is fail-closed against TOCTOU changes. Chain evidence
3387
+ is collected outside the store lock, then the exact durable snapshot
3388
+ is compared again inside the mutation transaction before deletion.
3389
+ """
3390
+
3391
+ initial = get_persistent_payment_intent(
3392
+ logical_intent_id
3393
+ )
3394
+
3395
+ if not isinstance(
3396
+ initial,
3397
+ Mapping,
3398
+ ):
3399
+ return False
3400
+
3401
+ if initial.get("state") != "submission_unknown":
3402
+ return False
3403
+
3404
+ # Never destroy an intent claim blindly. A claim may belong to a
3405
+ # process that is still alive. Stale-claim recovery is a separate
3406
+ # operation with its own safety proof.
3407
+ claim_path = _intent_claim_path(
3408
+ logical_intent_id
3409
+ )
3410
+
3411
+ if claim_path.exists():
3412
+ return False
3413
+
3414
+ snapshot = json.dumps(
3415
+ initial,
3416
+ sort_keys=True,
3417
+ separators=(",", ":"),
3418
+ ensure_ascii=False,
3419
+ )
3420
+
3421
+ preview = (
3422
+ _preview_eip3009_intent_recovery(
3423
+ logical_intent_id,
3424
+ now_unix=now_unix,
3425
+ read_authorization_state=(
3426
+ read_authorization_state
3427
+ ),
3428
+ read_authorization_event=(
3429
+ read_authorization_event
3430
+ ),
3431
+ )
3432
+ )
3433
+
3434
+ if not preview.get(
3435
+ "safe_to_release",
3436
+ False,
3437
+ ):
3438
+ return False
3439
+
3440
+ if preview.get(
3441
+ "requires_reconciliation",
3442
+ True,
3443
+ ):
3444
+ return False
3445
+
3446
+ intent_key = _persistent_intent_key(
3447
+ logical_intent_id
3448
+ )
3449
+
3450
+ def mutate(
3451
+ store: dict[str, Any],
3452
+ ):
3453
+ raw = store[
3454
+ "entries"
3455
+ ].get(intent_key)
3456
+
3457
+ if not isinstance(
3458
+ raw,
3459
+ Mapping,
3460
+ ):
3461
+ return False, False
3462
+
3463
+ if raw.get("state") != "submission_unknown":
3464
+ return False, False
3465
+
3466
+ current_snapshot = json.dumps(
3467
+ raw,
3468
+ sort_keys=True,
3469
+ separators=(",", ":"),
3470
+ ensure_ascii=False,
3471
+ )
3472
+
3473
+ # Anything changed after evidence was collected:
3474
+ # new authorization, timestamp, state, reconciliation data, etc.
3475
+ # Old evidence must not authorize deletion of newer state.
3476
+ if current_snapshot != snapshot:
3477
+ return False, False
3478
+
3479
+ # Re-check the claim immediately before economic release.
3480
+ if _intent_claim_path(
3481
+ logical_intent_id
3482
+ ).exists():
3483
+ return False, False
3484
+
3485
+ del store[
3486
+ "entries"
3487
+ ][intent_key]
3488
+
3489
+ return True, True
3490
+
3491
+ return _mutate_pending_store(
3492
+ mutate
3493
+ )
3494
+
3495
+
3496
+ def _make_payment_authorization_persistence_hook(
3497
+ logical_intent_id: str,
3498
+ ):
3499
+ """Persist non-secret recovery metadata after payment creation.
3500
+
3501
+ The signed payment payload exists at this point, but only the
3502
+ authorization envelope is persisted. The executable signature and
3503
+ private key are never written to durable state.
3504
+ """
3505
+
3506
+ def persist(context):
3507
+ payment_payload = getattr(
3508
+ context,
3509
+ "payment_payload",
3510
+ None,
3511
+ )
3512
+
3513
+ selected = getattr(
3514
+ context,
3515
+ "selected_requirements",
3516
+ None,
3517
+ )
3518
+
3519
+ if payment_payload is None:
3520
+ raise SettlementStoreError(
3521
+ "after-payment hook received no payment payload"
3522
+ )
3523
+
3524
+ if selected is None:
3525
+ raise SettlementStoreError(
3526
+ "after-payment hook received no selected requirements"
3527
+ )
3528
+
3529
+ payload = getattr(
3530
+ payment_payload,
3531
+ "payload",
3532
+ None,
3533
+ )
3534
+
3535
+ if not isinstance(
3536
+ payload,
3537
+ Mapping,
3538
+ ):
3539
+ raise SettlementStoreError(
3540
+ "after-payment hook received invalid payment payload"
3541
+ )
3542
+
3543
+ saved = (
3544
+ _record_payment_authorization_envelope(
3545
+ logical_intent_id,
3546
+ network=str(
3547
+ selected.network
3548
+ ),
3549
+ asset=str(
3550
+ selected.asset
3551
+ ),
3552
+ payload=payload,
3553
+ )
3554
+ )
3555
+
3556
+ if not saved:
3557
+ raise SettlementStoreError(
3558
+ "payment authorization envelope could not "
3559
+ "be persisted before paid request"
3560
+ )
3561
+
3562
+ return None
3563
+
3564
+ return persist
3565
+
3566
+
3567
+ def _record_payment_authorization_envelope(
3568
+ logical_intent_id: str,
3569
+ *,
3570
+ network: str,
3571
+ asset: str,
3572
+ payload: Mapping[str, Any],
3573
+ ) -> bool:
3574
+ """Persist non-secret authorization data for crash recovery.
3575
+
3576
+ The executable payment signature is deliberately never persisted.
3577
+ """
3578
+ intent_key = _persistent_intent_key(
3579
+ logical_intent_id
3580
+ )
3581
+
3582
+ authorization = payload.get(
3583
+ "authorization"
3584
+ )
3585
+
3586
+ if not isinstance(
3587
+ authorization,
3588
+ Mapping,
3589
+ ):
3590
+ raise SettlementStoreError(
3591
+ "payment authorization payload is missing"
3592
+ )
3593
+
3594
+ required_fields = (
3595
+ "from",
3596
+ "to",
3597
+ "value",
3598
+ "validAfter",
3599
+ "validBefore",
3600
+ "nonce",
3601
+ )
3602
+
3603
+ envelope: dict[str, str] = {}
3604
+
3605
+ for field in required_fields:
3606
+ value = authorization.get(
3607
+ field
3608
+ )
3609
+
3610
+ if value is None:
3611
+ raise SettlementStoreError(
3612
+ "payment authorization is missing "
3613
+ f"{field}"
3614
+ )
3615
+
3616
+ envelope[field] = str(
3617
+ value
3618
+ )
3619
+
3620
+ envelope["network"] = str(
3621
+ network
3622
+ )
3623
+ envelope["asset"] = str(
3624
+ asset
3625
+ )
3626
+
3627
+ now = datetime.now(
3628
+ timezone.utc
3629
+ ).isoformat()
3630
+
3631
+ def mutate(
3632
+ store: dict[str, Any],
3633
+ ):
3634
+ raw = store[
3635
+ "entries"
3636
+ ].get(intent_key)
3637
+
3638
+ if not isinstance(
3639
+ raw,
3640
+ Mapping,
3641
+ ):
3642
+ return False, False
3643
+
3644
+ if raw.get("state") != "submission_unknown":
3645
+ return False, False
3646
+
3647
+ entry = dict(raw)
3648
+
3649
+ history_raw = entry.get(
3650
+ "authorizations"
3651
+ )
3652
+
3653
+ if history_raw is None:
3654
+ history = []
3655
+
3656
+ legacy = entry.get(
3657
+ "authorization"
3658
+ )
3659
+
3660
+ if isinstance(
3661
+ legacy,
3662
+ Mapping,
3663
+ ):
3664
+ history.append(
3665
+ dict(legacy)
3666
+ )
3667
+
3668
+ elif isinstance(
3669
+ history_raw,
3670
+ list,
3671
+ ):
3672
+ history = [
3673
+ dict(item)
3674
+ if isinstance(item, Mapping)
3675
+ else item
3676
+ for item in history_raw
3677
+ ]
3678
+
3679
+ else:
3680
+ raise SettlementStoreError(
3681
+ "payment authorization history is invalid"
3682
+ )
3683
+
3684
+ nonce = envelope[
3685
+ "nonce"
3686
+ ]
3687
+
3688
+ for existing in history:
3689
+ if not isinstance(
3690
+ existing,
3691
+ Mapping,
3692
+ ):
3693
+ raise SettlementStoreError(
3694
+ "payment authorization history "
3695
+ "contains invalid entry"
3696
+ )
3697
+
3698
+ if str(
3699
+ existing.get(
3700
+ "nonce",
3701
+ "",
3702
+ )
3703
+ ) != nonce:
3704
+ continue
3705
+
3706
+ normalized_existing = {
3707
+ str(key): str(value)
3708
+ for key, value in existing.items()
3709
+ }
3710
+
3711
+ if normalized_existing != envelope:
3712
+ raise SettlementStoreError(
3713
+ "conflicting payment authorization "
3714
+ "envelope for existing nonce"
3715
+ )
3716
+
3717
+ # Idempotent replay of the exact same envelope.
3718
+ entry[
3719
+ "authorization"
3720
+ ] = envelope
3721
+
3722
+ entry[
3723
+ "authorizations"
3724
+ ] = history
3725
+
3726
+ store[
3727
+ "entries"
3728
+ ][intent_key] = entry
3729
+
3730
+ return True, False
3731
+
3732
+ history.append(
3733
+ envelope
3734
+ )
3735
+
3736
+ entry[
3737
+ "authorizations"
3738
+ ] = history
3739
+
3740
+ # Backwards-compatible alias for callers that need
3741
+ # the most recently created authorization.
3742
+ entry[
3743
+ "authorization"
3744
+ ] = envelope
3745
+
3746
+ entry[
3747
+ "updated_at"
3748
+ ] = now
3749
+
3750
+ store[
3751
+ "entries"
3752
+ ][intent_key] = entry
3753
+
3754
+ return True, True
3755
+
3756
+ return _mutate_pending_store(
3757
+ mutate
3758
+ )
3759
+
3760
+
3761
+ def _release_submission_unknown_after_terminal_failure(
3762
+ logical_intent_id: str,
3763
+ ) -> bool:
3764
+ """Release exposure only after an explicit terminal settlement failure."""
3765
+ intent_key = _persistent_intent_key(
3766
+ logical_intent_id
3767
+ )
3768
+
3769
+ def mutate(
3770
+ store: dict[str, Any],
3771
+ ):
3772
+ raw = store[
3773
+ "entries"
3774
+ ].get(intent_key)
3775
+
3776
+ if not isinstance(
3777
+ raw,
3778
+ Mapping,
3779
+ ):
3780
+ return False, False
3781
+
3782
+ if raw.get("state") != "submission_unknown":
3783
+ return False, False
3784
+
3785
+ del store[
3786
+ "entries"
3787
+ ][intent_key]
3788
+
3789
+ return True, True
3790
+
3791
+ return _mutate_pending_store(
3792
+ mutate
3793
+ )
3794
+
3795
+
3796
+ def _release_aggregate_budget_reservation(
3797
+ logical_intent_id: str,
3798
+ ) -> bool:
3799
+ """Release only a still-unbroadcast reservation."""
3800
+
3801
+ intent_key = _persistent_intent_key(
3802
+ logical_intent_id
3803
+ )
3804
+
3805
+ def mutate(
3806
+ store: dict[str, Any],
3807
+ ):
3808
+ raw = store[
3809
+ "entries"
3810
+ ].get(intent_key)
3811
+
3812
+ if not isinstance(
3813
+ raw,
3814
+ Mapping,
3815
+ ):
3816
+ return False, False
3817
+
3818
+ if raw.get("state") != "reserved":
3819
+ return False, False
3820
+
3821
+ del store[
3822
+ "entries"
3823
+ ][intent_key]
3824
+
3825
+ return True, True
3826
+
3827
+ return _mutate_pending_store(
3828
+ mutate
3829
+ )
3830
+
3831
+
3832
+ def _record_pending_settlement(
3833
+ resource: str,
3834
+ requirement: Any,
3835
+ transaction: str,
3836
+ network: str = BASE_SEPOLIA,
3837
+ *,
3838
+ intent_id: str | None = None,
3839
+ agent_id: str | None = None,
3840
+ session_id: str | None = None,
3841
+ ) -> dict[str, Any]:
3842
+ logical_intent_id = _resolve_payment_intent_id(
3843
+ intent_id,
3844
+ resource,
3845
+ requirement,
3846
+ )
3847
+ intent_key = _persistent_intent_key(logical_intent_id)
3848
+
3849
+ normalized_agent_id = None
3850
+
3851
+ if agent_id is not None:
3852
+ from qruntime.spend import (
3853
+ _normalize_agent_id,
3854
+ )
3855
+
3856
+ normalized_agent_id = (
3857
+ _normalize_agent_id(
3858
+ agent_id
3859
+ )
3860
+ )
3861
+
3862
+ normalized_session_id = None
3863
+
3864
+ if session_id is not None:
3865
+ from qruntime.spend import (
3866
+ _normalize_session_id,
3867
+ )
3868
+
3869
+ normalized_session_id = (
3870
+ _normalize_session_id(
3871
+ session_id
3872
+ )
3873
+ )
3874
+
3875
+ fingerprint = requirement.fingerprint()
3876
+ now = datetime.now(timezone.utc).isoformat()
3877
+
3878
+ entry = {
3879
+ "intent_id": logical_intent_id,
3880
+ "intent_key": intent_key,
3881
+ "resource": resource,
3882
+ "agent_id": normalized_agent_id,
3883
+ "session_id": normalized_session_id,
3884
+ "quote_fingerprint": fingerprint,
3885
+ "requirement": requirement.canonical_dict(),
3886
+ "transaction": transaction,
3887
+ "network": network,
3888
+ "state": "pending",
3889
+ "first_seen_at": now,
3890
+ "updated_at": now,
3891
+ }
3892
+
3893
+ def mutate(store: dict[str, Any]):
3894
+ pending_entry = dict(entry)
3895
+
3896
+ previous = store[
3897
+ "entries"
3898
+ ].get(intent_key)
3899
+
3900
+ if (
3901
+ isinstance(
3902
+ previous,
3903
+ Mapping,
3904
+ )
3905
+ and previous.get("state")
3906
+ == "reserved"
3907
+ and isinstance(
3908
+ previous.get("reservation"),
3909
+ Mapping,
3910
+ )
3911
+ ):
3912
+ pending_entry[
3913
+ "reservation"
3914
+ ] = dict(
3915
+ previous["reservation"]
3916
+ )
3917
+
3918
+ previous_agent_id = (
3919
+ previous.get("agent_id")
3920
+ )
3921
+
3922
+ if (
3923
+ previous_agent_id is not None
3924
+ and normalized_agent_id is not None
3925
+ and previous_agent_id
3926
+ != normalized_agent_id
3927
+ ):
3928
+ raise SettlementStoreError(
3929
+ "reserved payment agent identity "
3930
+ "does not match settlement agent"
3931
+ )
3932
+
3933
+ if previous_agent_id is not None:
3934
+ pending_entry[
3935
+ "agent_id"
3936
+ ] = previous_agent_id
3937
+
3938
+ previous_session_id = (
3939
+ previous.get("session_id")
3940
+ )
3941
+
3942
+ if (
3943
+ previous_session_id is not None
3944
+ and normalized_session_id is not None
3945
+ and previous_session_id
3946
+ != normalized_session_id
3947
+ ):
3948
+ raise SettlementStoreError(
3949
+ "reserved payment session identity "
3950
+ "does not match settlement session"
3951
+ )
3952
+
3953
+ if previous_session_id is not None:
3954
+ pending_entry[
3955
+ "session_id"
3956
+ ] = previous_session_id
3957
+
3958
+ store[
3959
+ "entries"
3960
+ ][intent_key] = pending_entry
3961
+
3962
+ return pending_entry, True
3963
+
3964
+ return _mutate_pending_store(mutate)
3965
+
3966
+
3967
+ def _assert_no_persistent_payment_conflict(
3968
+ resource: str,
3969
+ requirement: Any,
3970
+ *,
3971
+ intent_id: str | None = None,
3972
+ ) -> None:
3973
+ logical_intent_id = _resolve_payment_intent_id(
3974
+ intent_id,
3975
+ resource,
3976
+ requirement,
3977
+ )
3978
+
3979
+ intent_key = _persistent_intent_key(
3980
+ logical_intent_id
3981
+ )
3982
+
3983
+ def inspect(
3984
+ store: Mapping[str, Any],
3985
+ ) -> tuple[None, bool]:
3986
+ entry = store[
3987
+ "entries"
3988
+ ].get(
3989
+ intent_key
3990
+ )
3991
+
3992
+ if not isinstance(
3993
+ entry,
3994
+ Mapping,
3995
+ ):
3996
+ return None, False
3997
+
3998
+ stored_resource = str(
3999
+ entry.get(
4000
+ "resource",
4001
+ "",
4002
+ )
4003
+ )
4004
+
4005
+ if (
4006
+ stored_resource
4007
+ and stored_resource
4008
+ != resource
4009
+ ):
4010
+ raise IntentResourceMismatchError(
4011
+ "preflight refused payment: intent_id is already bound "
4012
+ "to a different resource"
4013
+ )
4014
+
4015
+ state = str(
4016
+ entry.get(
4017
+ "state",
4018
+ "",
4019
+ )
4020
+ )
4021
+
4022
+ if state == "reserved":
4023
+ raise IntentInFlightError(
4024
+ "preflight refused payment: durable budget "
4025
+ f"reservation exists for intent {logical_intent_id}; "
4026
+ "reservation must be explicitly released or "
4027
+ "advanced before retry"
4028
+ )
4029
+
4030
+ if state == "submission_unknown":
4031
+ raise IntentInFlightError(
4032
+ "preflight refused payment: submission outcome "
4033
+ "is unknown for intent "
4034
+ f"{logical_intent_id}; economic exposure must "
4035
+ "be reconciled before retry"
4036
+ )
4037
+
4038
+ if state == "pending":
4039
+ raise SettlementPendingError(
4040
+ "preflight refused payment: unresolved prior settlement "
4041
+ f"{entry.get('transaction')} exists for intent "
4042
+ f"{logical_intent_id}; reconcile before retry",
4043
+ transaction=(
4044
+ str(
4045
+ entry.get(
4046
+ "transaction"
4047
+ )
4048
+ )
4049
+ if entry.get(
4050
+ "transaction"
4051
+ )
4052
+ else None
4053
+ ),
4054
+ network=(
4055
+ str(
4056
+ entry.get(
4057
+ "network"
4058
+ )
4059
+ )
4060
+ if entry.get(
4061
+ "network"
4062
+ )
4063
+ else None
4064
+ ),
4065
+ )
4066
+
4067
+ if state == "resolved_confirmed":
4068
+ raise DuplicateIntentError(
4069
+ "preflight refused payment: prior settlement for intent "
4070
+ f"{logical_intent_id} is already confirmed; "
4071
+ "refusing duplicate economic effect"
4072
+ )
4073
+
4074
+ return None, False
4075
+
4076
+ _mutate_pending_store(
4077
+ inspect
4078
+ )
4079
+
4080
+
4081
+
4082
+
4083
+
4084
+ def _record_confirmed_settlement(
4085
+ resource: str,
4086
+ requirement: Any,
4087
+ transaction: str,
4088
+ network: str = BASE_SEPOLIA,
4089
+ *,
4090
+ intent_id: str | None = None,
4091
+ agent_id: str | None = None,
4092
+ session_id: str | None = None,
4093
+ ) -> None:
4094
+ """Durably tombstone a successful economic effect.
4095
+
4096
+ Write pending first so a crash between writes remains fail-closed.
4097
+ """
4098
+ _record_pending_settlement(
4099
+ resource,
4100
+ requirement,
4101
+ transaction,
4102
+ network,
4103
+ intent_id=intent_id,
4104
+ agent_id=agent_id,
4105
+ session_id=session_id,
4106
+ )
4107
+
4108
+ if not _update_persistent_reconciliation(
4109
+ transaction,
4110
+ "confirmed",
4111
+ ):
4112
+ raise SettlementStoreError(
4113
+ "successful settlement could not be marked confirmed "
4114
+ "in persistent state"
4115
+ )
4116
+
4117
+
4118
+ def _update_persistent_reconciliation(
4119
+ transaction: str,
4120
+ outcome: str,
4121
+ ) -> bool:
4122
+ if outcome not in {"confirmed", "failed"}:
4123
+ return False
4124
+
4125
+ now = datetime.now(timezone.utc).isoformat()
4126
+
4127
+ def mutate(store: dict[str, Any]):
4128
+ changed = False
4129
+
4130
+ for key, raw_entry in list(store["entries"].items()):
4131
+ if not isinstance(raw_entry, Mapping):
4132
+ continue
4133
+
4134
+ if (
4135
+ str(raw_entry.get("transaction", "")).lower()
4136
+ != transaction.lower()
4137
+ ):
4138
+ continue
4139
+
4140
+ entry = dict(raw_entry)
4141
+ entry["state"] = (
4142
+ "resolved_confirmed"
4143
+ if outcome == "confirmed"
4144
+ else "resolved_failed"
4145
+ )
4146
+ entry["reconciliation_outcome"] = outcome
4147
+ entry["updated_at"] = now
4148
+
4149
+ store["entries"][key] = entry
4150
+ changed = True
4151
+
4152
+ return changed, changed
4153
+
4154
+ return _mutate_pending_store(mutate)
4155
+
4156
+
4157
+ def _fetch_base_sepolia_transaction(
4158
+ transaction: str,
4159
+ timeout: float,
4160
+ ) -> dict[str, Any] | None:
4161
+ """Read one Base-Sepolia transaction from Blockscout.
4162
+
4163
+ None means "not currently found", never terminal failure.
4164
+ """
4165
+ from urllib.error import HTTPError, URLError
4166
+ from urllib.request import Request, urlopen
4167
+
4168
+ endpoint = (
4169
+ "https://base-sepolia.blockscout.com/api/v2/transactions/"
4170
+ f"{transaction}"
4171
+ )
4172
+ request = Request(
4173
+ endpoint,
4174
+ headers={
4175
+ "Accept": "application/json",
4176
+ "User-Agent": "q-runtime/0.6",
4177
+ },
4178
+ )
4179
+
4180
+ try:
4181
+ with urlopen(request, timeout=timeout) as response:
4182
+ raw = json.loads(response.read().decode("utf-8"))
4183
+ except HTTPError as exc:
4184
+ if exc.code == 404:
4185
+ return None
4186
+ raise ReconciliationError(
4187
+ f"Blockscout reconciliation HTTP {exc.code}"
4188
+ ) from exc
4189
+ except (URLError, TimeoutError) as exc:
4190
+ raise ReconciliationError(
4191
+ f"Blockscout reconciliation unavailable: {exc}"
4192
+ ) from exc
4193
+
4194
+ if not isinstance(raw, Mapping):
4195
+ raise ReconciliationError(
4196
+ "Blockscout reconciliation returned a non-object response"
4197
+ )
4198
+
4199
+ return dict(raw)
4200
+
4201
+
4202
+ def reconcile_testnet_transaction(
4203
+ transaction: str,
4204
+ *,
4205
+ out: str | Path,
4206
+ timeout: float = 15.0,
4207
+ ) -> dict[str, Any]:
4208
+ """Reconcile an already-broadcast Base-Sepolia transaction.
4209
+
4210
+ This function is read-only. It never signs, broadcasts, or retries payment.
4211
+ """
4212
+ import re
4213
+
4214
+ tx = transaction.strip()
4215
+
4216
+ if not re.fullmatch(r"0x[a-fA-F0-9]{64}", tx):
4217
+ raise ValueError(
4218
+ "transaction must be a 0x-prefixed 32-byte hash"
4219
+ )
4220
+
4221
+ if timeout <= 0:
4222
+ raise ValueError("timeout must be positive")
4223
+
4224
+ info = _fetch_base_sepolia_transaction(tx, timeout)
4225
+
4226
+ if info is None:
4227
+ outcome = "pending"
4228
+ reason = (
4229
+ "transaction is not currently visible; "
4230
+ "absence is not treated as terminal"
4231
+ )
4232
+ status = None
4233
+ else:
4234
+ status = str(info.get("status", "")).lower()
4235
+
4236
+ if status == "ok":
4237
+ outcome = "confirmed"
4238
+ reason = "transaction confirmed successfully on Base Sepolia"
4239
+ elif status == "error":
4240
+ outcome = "failed"
4241
+ reason = "transaction confirmed with failure on Base Sepolia"
4242
+ else:
4243
+ outcome = "pending"
4244
+ reason = (
4245
+ "transaction exists but does not yet have a terminal status"
4246
+ )
4247
+
4248
+ persistent_state_updated = _update_persistent_reconciliation(
4249
+ tx,
4250
+ outcome,
4251
+ )
4252
+
4253
+ report = {
4254
+ "format": "qruntime-x402-reconciliation-v1",
4255
+ "persistent_state_updated": persistent_state_updated,
4256
+ "checked_at": datetime.now(timezone.utc).isoformat(),
4257
+ "network": BASE_SEPOLIA,
4258
+ "transaction": tx,
4259
+ "outcome": outcome,
4260
+ "terminal": outcome in {"confirmed", "failed"},
4261
+ "reason": reason,
4262
+ "source": "base-sepolia.blockscout.com",
4263
+ "chain": {
4264
+ "status": status,
4265
+ "block_number": None if info is None else info.get("block_number"),
4266
+ "confirmations": None if info is None else info.get("confirmations"),
4267
+ "method": None if info is None else info.get("method"),
4268
+ },
4269
+ }
4270
+
4271
+ destination = Path(out)
4272
+ destination.parent.mkdir(parents=True, exist_ok=True)
4273
+ destination.write_text(
4274
+ json.dumps(report, indent=2, ensure_ascii=False),
4275
+ encoding="utf-8",
4276
+ )
4277
+
4278
+ return report
4279
+
4280
+
4281
+ def run_local_testnet_payment(
4282
+ url: str,
4283
+ *,
4284
+ max_amount_atomic: int,
4285
+ out: str | Path,
4286
+ intent_id: str | None = None,
4287
+ key_env: str = DEFAULT_KEY_ENV,
4288
+ timeout: float = 30.0,
4289
+ policy: Policy | None = None,
4290
+ budget: Any | None = None,
4291
+ agent_id: str | None = None,
4292
+ session_id: str | None = None,
4293
+ ) -> dict[str, Any]:
4294
+ """Run one controlled Base-Sepolia x402 payment against localhost.
4295
+
4296
+ Safety properties:
4297
+ - only http(s)://127.0.0.1 or localhost is accepted;
4298
+ - only exact / eip155:84532 is accepted;
4299
+ - preflight amount must be <= max_amount_atomic;
4300
+ - private key is read from an environment variable and never serialized.
4301
+ """
4302
+ parsed_url = urlparse(url)
4303
+ if parsed_url.scheme not in {"http", "https"} or parsed_url.hostname not in {"127.0.0.1", "localhost"}:
4304
+ raise ValueError("live-testnet is localhost-only; use the controlled Q Runtime demo server")
4305
+ if max_amount_atomic <= 0:
4306
+ raise ValueError("--max-amount-atomic must be positive")
4307
+
4308
+ try:
4309
+ capture = capture_payment_required(url, timeout=min(timeout, 15.0))
4310
+ except X402CaptureError as exc:
4311
+ raise RuntimeError(f"preflight 402 capture failed: {exc}") from exc
4312
+
4313
+ candidates = [
4314
+ req for req in capture.payment_required.accepts
4315
+ if req.scheme == "exact" and req.network == BASE_SEPOLIA and req.amount_atomic <= max_amount_atomic
4316
+ ]
4317
+ if not candidates:
4318
+ offered = [
4319
+ {"scheme": r.scheme, "network": r.network, "amount": r.amount_atomic}
4320
+ for r in capture.payment_required.accepts
4321
+ ]
4322
+ raise PaymentRequirementRejectedError(
4323
+ f"preflight refused payment: no exact/{BASE_SEPOLIA} requirement within cap "
4324
+ f"{max_amount_atomic}; offered={offered}"
4325
+ )
4326
+ selected = candidates[0]
4327
+ if capture.payment_required.resource_url != url:
4328
+ raise PaymentRequirementMismatchError(
4329
+ "preflight refused payment: PAYMENT-REQUIRED resource.url does not exactly match controlled request URL"
4330
+ )
4331
+
4332
+ if policy is not None:
4333
+ decision = evaluate_policy(
4334
+ policy,
4335
+ amount_atomic=selected.amount_atomic,
4336
+ origin=url,
4337
+ asset=selected.asset,
4338
+ )
4339
+
4340
+ if not decision.allowed:
4341
+ raise PolicyDeniedError(
4342
+ decision
4343
+ )
4344
+
4345
+ logical_intent_id = _resolve_payment_intent_id(
4346
+ intent_id,
4347
+ url,
4348
+ selected,
4349
+ )
4350
+ _assert_no_persistent_payment_conflict(
4351
+ url,
4352
+ selected,
4353
+ intent_id=logical_intent_id,
4354
+ )
4355
+
4356
+ intent_claim = _acquire_payment_intent_claim(
4357
+ logical_intent_id,
4358
+ url,
4359
+ )
4360
+
4361
+ budget_reserved = False
4362
+
4363
+ if budget is not None:
4364
+ try:
4365
+ from qruntime.budget import (
4366
+ BudgetSet,
4367
+ )
4368
+
4369
+ if isinstance(
4370
+ budget,
4371
+ BudgetSet,
4372
+ ):
4373
+ budget_decision = (
4374
+ _reserve_budget_set(
4375
+ logical_intent_id,
4376
+ url,
4377
+ selected,
4378
+ budget,
4379
+ agent_id=agent_id,
4380
+ session_id=session_id,
4381
+ )
4382
+ )
4383
+ else:
4384
+ budget_decision = (
4385
+ _reserve_aggregate_budget(
4386
+ logical_intent_id,
4387
+ url,
4388
+ selected,
4389
+ budget,
4390
+ agent_id=agent_id,
4391
+ session_id=session_id,
4392
+ )
4393
+ )
4394
+ except Exception:
4395
+ _release_payment_intent_claim(
4396
+ intent_claim
4397
+ )
4398
+ raise
4399
+
4400
+ if not budget_decision.allowed:
4401
+ _release_payment_intent_claim(
4402
+ intent_claim
4403
+ )
4404
+
4405
+ from qruntime.exceptions import (
4406
+ BudgetDeniedError,
4407
+ )
4408
+
4409
+ denied_decision = getattr(
4410
+ budget_decision,
4411
+ "first_denied",
4412
+ None,
4413
+ )
4414
+
4415
+ if denied_decision is None:
4416
+ denied_decision = (
4417
+ budget_decision
4418
+ )
4419
+
4420
+ raise BudgetDeniedError(
4421
+ denied_decision
4422
+ )
4423
+
4424
+ budget_reserved = True
4425
+
4426
+ try:
4427
+ private_key = os.environ.get(key_env, "").strip()
4428
+ if not private_key:
4429
+ raise RuntimeError(
4430
+ f"missing testnet private key environment variable: {key_env}"
4431
+ )
4432
+
4433
+ try:
4434
+ from eth_account import Account
4435
+ from x402 import AbortResult, x402ClientSync, max_amount
4436
+ from x402.http.clients import x402_requests
4437
+ from x402.mechanisms.evm import EthAccountSigner
4438
+ from x402.mechanisms.evm.exact.register import register_exact_evm_client
4439
+ except ImportError as exc: # pragma: no cover
4440
+ raise RuntimeError(
4441
+ "official x402 client dependencies are missing. Install with: "
4442
+ 'python -m pip install -e ".[official-x402]"'
4443
+ ) from exc
4444
+
4445
+ account = Account.from_key(private_key)
4446
+ client = x402ClientSync()
4447
+ client.register_policy(max_amount(max_amount_atomic))
4448
+ client.on_before_payment_creation(
4449
+ _make_live_quote_binding_hook(selected, AbortResult)
4450
+ )
4451
+ client.on_after_payment_creation(
4452
+ _make_payment_authorization_persistence_hook(
4453
+ logical_intent_id
4454
+ )
4455
+ )
4456
+ register_exact_evm_client(
4457
+ client,
4458
+ EthAccountSigner(account),
4459
+ )
4460
+
4461
+ except Exception:
4462
+ if budget_reserved:
4463
+ released = (
4464
+ _release_aggregate_budget_reservation(
4465
+ logical_intent_id
4466
+ )
4467
+ )
4468
+
4469
+ if not released:
4470
+ _release_payment_intent_claim(
4471
+ intent_claim
4472
+ )
4473
+
4474
+ raise SettlementStoreError(
4475
+ "pre-broadcast failure could not "
4476
+ "release durable budget reservation; "
4477
+ "refusing to free budget silently"
4478
+ )
4479
+
4480
+ _release_payment_intent_claim(
4481
+ intent_claim
4482
+ )
4483
+ raise
4484
+
4485
+ try:
4486
+ submission_marked = (
4487
+ _mark_payment_submission_unknown(
4488
+ logical_intent_id,
4489
+ url,
4490
+ selected,
4491
+ agent_id=agent_id,
4492
+ session_id=session_id,
4493
+ )
4494
+ )
4495
+ except Exception:
4496
+ if budget_reserved:
4497
+ released = (
4498
+ _release_aggregate_budget_reservation(
4499
+ logical_intent_id
4500
+ )
4501
+ )
4502
+
4503
+ if not released:
4504
+ _release_payment_intent_claim(
4505
+ intent_claim
4506
+ )
4507
+
4508
+ raise SettlementStoreError(
4509
+ "pre-handoff failure could not release "
4510
+ "durable budget reservation"
4511
+ )
4512
+
4513
+ _release_payment_intent_claim(
4514
+ intent_claim
4515
+ )
4516
+ raise
4517
+
4518
+ if not submission_marked:
4519
+ if budget_reserved:
4520
+ _release_aggregate_budget_reservation(
4521
+ logical_intent_id
4522
+ )
4523
+
4524
+ _release_payment_intent_claim(
4525
+ intent_claim
4526
+ )
4527
+
4528
+ raise SettlementStoreError(
4529
+ "payment submission state could not be "
4530
+ "persisted before network handoff"
4531
+ )
4532
+
4533
+ started = datetime.now(timezone.utc).isoformat()
4534
+ with x402_requests(client) as session:
4535
+ response = session.get(url, timeout=timeout)
4536
+ finished = datetime.now(timezone.utc).isoformat()
4537
+
4538
+ settlement_header = _header_ci(response.headers, "PAYMENT-RESPONSE", "X-PAYMENT-RESPONSE")
4539
+ if not settlement_header:
4540
+ raise SettlementReceiptError(
4541
+ f"paid request returned HTTP {response.status_code} without PAYMENT-RESPONSE settlement receipt"
4542
+ )
4543
+ settlement = parse_settlement_response(settlement_header)
4544
+
4545
+ if settlement.reconciliation_required:
4546
+ _record_pending_settlement(
4547
+ url,
4548
+ selected,
4549
+ settlement.transaction,
4550
+ settlement.network,
4551
+ intent_id=logical_intent_id,
4552
+ agent_id=agent_id,
4553
+ session_id=session_id,
4554
+ )
4555
+ raise SettlementPendingError(
4556
+ "settlement pending after broadcast; transaction "
4557
+ f"{settlement.transaction} was persisted and must be reconciled "
4558
+ "before retry",
4559
+ transaction=settlement.transaction,
4560
+ network=settlement.network,
4561
+ )
4562
+
4563
+ if settlement.success:
4564
+ if settlement.network != BASE_SEPOLIA:
4565
+ raise SettlementReceiptError(
4566
+ f"unexpected settlement network: {settlement.network}"
4567
+ )
4568
+ if not settlement.transaction:
4569
+ raise SettlementReceiptError(
4570
+ "successful settlement did not return a transaction hash"
4571
+ )
4572
+
4573
+ _record_confirmed_settlement(
4574
+ url,
4575
+ selected,
4576
+ settlement.transaction,
4577
+ settlement.network,
4578
+ intent_id=logical_intent_id,
4579
+ agent_id=agent_id,
4580
+ session_id=session_id,
4581
+ )
4582
+ _release_payment_intent_claim(intent_claim)
4583
+
4584
+ if not settlement.success:
4585
+ released = (
4586
+ _release_submission_unknown_after_terminal_failure(
4587
+ logical_intent_id
4588
+ )
4589
+ )
4590
+
4591
+ if not released:
4592
+ raise SettlementStoreError(
4593
+ "terminal settlement failure could not "
4594
+ "release durable economic exposure"
4595
+ )
4596
+
4597
+ _release_payment_intent_claim(
4598
+ intent_claim
4599
+ )
4600
+
4601
+ raise SettlementFailedError(
4602
+ f"settlement failed: {settlement.error_reason or 'unknown'}",
4603
+ reason=settlement.error_reason,
4604
+ )
4605
+
4606
+ if response.status_code < 200 or response.status_code >= 300:
4607
+ raise PaidResourceError(
4608
+ f"paid request did not succeed: HTTP {response.status_code}",
4609
+ status_code=response.status_code,
4610
+ transaction=settlement.transaction or None,
4611
+ network=settlement.network or None,
4612
+ )
4613
+
4614
+ report = {
4615
+ "format": "qruntime-x402-live-testnet-v1",
4616
+ "started_at": started,
4617
+ "finished_at": finished,
4618
+ "safety": {
4619
+ "localhost_only": True,
4620
+ "network": BASE_SEPOLIA,
4621
+ "scheme": "exact",
4622
+ "max_amount_atomic": max_amount_atomic,
4623
+ "private_key_recorded": False,
4624
+ },
4625
+ "payer": account.address,
4626
+ "request_url": url,
4627
+ "intent_id": logical_intent_id,
4628
+ "preflight": {
4629
+ "status": capture.status_code,
4630
+ "resource": capture.payment_required.resource_url,
4631
+ "quote_fingerprint": selected.fingerprint(),
4632
+ "amount_atomic": selected.amount_atomic,
4633
+ "asset": selected.asset,
4634
+ "pay_to": selected.pay_to,
4635
+ },
4636
+ "paid_response": {
4637
+ "http_status": response.status_code,
4638
+ "body_preview": response.text[:2000],
4639
+ },
4640
+ "settlement": {
4641
+ "success": settlement.success,
4642
+ "transaction": settlement.transaction,
4643
+ "network": settlement.network,
4644
+ "payer": settlement.payer,
4645
+ "amount_atomic": settlement.amount_atomic,
4646
+ },
4647
+ }
4648
+ destination = Path(out)
4649
+ destination.parent.mkdir(parents=True, exist_ok=True)
4650
+ destination.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
4651
+ return report