patchr 0.1.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.
Files changed (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,950 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from collections.abc import Callable
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any, Mapping
10
+
11
+ from .adapters import PayRailAdapter, PayRailAdapterContext, defaultAdapters
12
+ from .models import Money, ProofOfValue, SettlementAdapterSpec, SettlementRequest
13
+
14
+ SignatureVerifier = Callable[[dict[str, Any], str, str], bool]
15
+
16
+
17
+ class PayDomain:
18
+ """Mandate and settlement engine for Picux PAY."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ verifier: SignatureVerifier | None = None,
24
+ clock: Callable[[], datetime] | None = None,
25
+ env: Mapping[str, str] | None = None,
26
+ adapters: list[PayRailAdapter] | None = None,
27
+ ) -> None:
28
+ self.verifier = verifier
29
+ self.clock = clock or (lambda: datetime.now(timezone.utc))
30
+ self.env = dict(os.environ if env is None else env)
31
+ self.adapterRunners = {adapter.adapterId: adapter for adapter in (adapters or defaultAdapters())}
32
+ self.challenges: dict[str, dict[str, Any]] = {}
33
+ self.verifiedCredentials: dict[str, dict[str, Any]] = {}
34
+ self.sessions: dict[str, dict[str, Any]] = {}
35
+ self.reputationCards: dict[str, dict[str, Any]] = {}
36
+
37
+ def validateMandate(self, mandate: dict[str, Any]) -> dict[str, Any]:
38
+ errors: list[str] = []
39
+ normalized = self._normalizeMandate(mandate)
40
+ kind = self.mandateKind(normalized)
41
+ required = ("mandateId", "issuer", "validUntil", "signature")
42
+ if kind == "payment":
43
+ required = (*required, "payer", "payee", "amount", "rail")
44
+ else:
45
+ required = (*required, "constraints")
46
+ for key in required:
47
+ if not normalized.get(key):
48
+ errors.append(f"missing:{key}")
49
+ issuer = normalized.get("issuer", {}) if isinstance(normalized.get("issuer"), dict) else {}
50
+ if not issuer.get("entityId"):
51
+ errors.append("missing:issuer.entityId")
52
+ if not issuer.get("publicKey"):
53
+ errors.append("missing:issuer.publicKey")
54
+
55
+ if kind == "payment":
56
+ spend = self._paymentAmount(normalized)
57
+ payerId = self.payerId(normalized)
58
+ payeeId = self.payeeId(normalized)
59
+ rail = self.rail(normalized)
60
+ if not payerId:
61
+ errors.append("missing:payer.entityId")
62
+ if not payeeId:
63
+ errors.append("missing:payee.entityId")
64
+ if spend.amount <= 0:
65
+ errors.append("invalid:amount")
66
+ if not spend.currency:
67
+ errors.append("missing:amount.currency")
68
+ if not rail.get("railId"):
69
+ errors.append("missing:rail.railId")
70
+ budget = self._constraintMaxSpend(normalized)
71
+ if budget.amount > 0:
72
+ if spend.amount > budget.amount:
73
+ errors.append("amountOverMaxSpend")
74
+ if spend.currency != budget.currency:
75
+ errors.append("amountCurrencyMismatch")
76
+ else:
77
+ spend = self._constraintMaxSpend(normalized)
78
+ if spend.amount <= 0:
79
+ errors.append("invalid:maxSpend.amount")
80
+ if not spend.currency:
81
+ errors.append("missing:maxSpend.currency")
82
+
83
+ validUntil = str(normalized.get("validUntil", "") or "")
84
+ expiresAt = self._parseTime(validUntil)
85
+ if expiresAt is None:
86
+ errors.append("invalid:validUntil")
87
+ elif expiresAt <= self.clock():
88
+ errors.append("expired")
89
+
90
+ signature = str(normalized.get("signature", "") or "")
91
+ publicKey = str(issuer.get("publicKey", "") or "")
92
+ sigOk = False
93
+ if signature and publicKey and self.verifier is not None:
94
+ sigOk = bool(self.verifier(self.canonicalPayload(normalized), signature, publicKey))
95
+ elif signature and publicKey:
96
+ errors.append("signatureVerifierMissing")
97
+ if self.verifier is not None and not sigOk:
98
+ errors.append("signatureInvalid")
99
+
100
+ return {
101
+ "ok": not errors,
102
+ "errors": errors,
103
+ "kind": kind,
104
+ "mandateId": str(normalized.get("mandateId", "") or ""),
105
+ "maxSpend": spend.toMap(),
106
+ "allowedVendors": self.allowedVendors(normalized),
107
+ "resolveRules": self.resolveRules(normalized),
108
+ "payerId": self.payerId(normalized),
109
+ "payeeId": self.payeeId(normalized),
110
+ "rail": self.rail(normalized),
111
+ "signatureOk": sigOk,
112
+ }
113
+
114
+ def canSettle(self, mandate: dict[str, Any], request: SettlementRequest | dict[str, Any]) -> dict[str, Any]:
115
+ req = request if isinstance(request, SettlementRequest) else SettlementRequest.fromObj(request)
116
+ validation = self.validateMandate(mandate)
117
+ errors = list(validation["errors"])
118
+ if not req.taskId:
119
+ errors.append("missing:taskId")
120
+ if not req.vendorId:
121
+ errors.append("missing:vendorId")
122
+ if req.amount.amount <= 0:
123
+ errors.append("invalid:amount")
124
+
125
+ maxSpend = Money.fromObj(validation.get("maxSpend", {}))
126
+ if req.amount.currency != maxSpend.currency:
127
+ errors.append("currencyMismatch")
128
+ if req.amount.amount > maxSpend.amount:
129
+ errors.append("overBudget")
130
+
131
+ allowed = validation.get("allowedVendors", [])
132
+ if allowed and req.vendorId not in allowed:
133
+ errors.append("vendorNotAllowed")
134
+
135
+ missingRules = [rule for rule in validation.get("resolveRules", []) if rule not in req.pov.rules]
136
+ if missingRules:
137
+ errors.append("missingProofRules")
138
+ if validation.get("resolveRules") and not req.pov.povId:
139
+ errors.append("missing:povId")
140
+
141
+ return {
142
+ "ok": not errors,
143
+ "errors": errors,
144
+ "taskId": req.taskId,
145
+ "vendorId": req.vendorId,
146
+ "amount": req.amount.toMap(),
147
+ "pov": req.pov.toMap(),
148
+ }
149
+
150
+ def settle(
151
+ self,
152
+ mandate: dict[str, Any],
153
+ request: SettlementRequest | dict[str, Any],
154
+ adapter: SettlementAdapterSpec | dict[str, Any] | None = None,
155
+ ) -> dict[str, Any]:
156
+ normalized = self._normalizeMandate(mandate)
157
+ req = request if isinstance(request, SettlementRequest) else SettlementRequest.fromObj(request)
158
+ decision = self.canSettle(normalized, req)
159
+ status = "settled" if decision["ok"] else "rejected"
160
+ receipt = self.receipt(
161
+ mandateId=str(normalized.get("mandateId", "") or ""),
162
+ taskId=req.taskId,
163
+ status=status,
164
+ amount=req.amount,
165
+ pov=req.pov,
166
+ errors=decision["errors"],
167
+ )
168
+ result: dict[str, Any] = {"ok": decision["ok"], "decision": decision, "receipt": receipt}
169
+ if not decision["ok"]:
170
+ return result
171
+ adapterSpec = adapter if isinstance(adapter, SettlementAdapterSpec) else SettlementAdapterSpec.fromObj(adapter)
172
+ rail = self.rail(normalized)
173
+ railId = str(rail.get("railId", adapterSpec.rail) or adapterSpec.rail)
174
+ adapterRunner = self.adapterRunners.get(adapterSpec.adapterId) or self._adapterForRail(railId)
175
+ if adapterRunner is None:
176
+ return result
177
+ prepared = self.prepareSettlement(normalized, request, adapterSpec)
178
+ instruction = prepared.get("instruction", {}) if isinstance(prepared.get("instruction"), dict) else {}
179
+ extras = request if isinstance(request, dict) else {}
180
+ executionPayload = {
181
+ "instruction": {
182
+ **instruction,
183
+ **{key: extras[key] for key in ("signedTransaction", "transaction", "live", "execute", "mission") if key in extras},
184
+ }
185
+ }
186
+ adapterExecution = adapterRunner.invoke("settlement.execute", executionPayload, self._adapterContext())
187
+ result["prepare"] = prepared
188
+ result["adapterExecution"] = adapterExecution
189
+ result["rail"] = railId
190
+ if not adapterExecution.get("ok") and railId not in {"", "offchain", "picuxLedger"}:
191
+ result["ok"] = False
192
+ result["receipt"] = {**receipt, "status": "pendingExternalSettlement", "errors": [*receipt.get("errors", []), str(adapterExecution.get("error", "adapterExecutionFailed") or "adapterExecutionFailed")]}
193
+ result["decision"] = {**decision, "errors": [*decision.get("errors", []), str(adapterExecution.get("error", "adapterExecutionFailed") or "adapterExecutionFailed")]}
194
+ return result
195
+
196
+ def prepareSettlement(
197
+ self,
198
+ mandate: dict[str, Any],
199
+ request: SettlementRequest | dict[str, Any],
200
+ adapter: SettlementAdapterSpec | dict[str, Any] | None = None,
201
+ ) -> dict[str, Any]:
202
+ normalized = self._normalizeMandate(mandate)
203
+ req = request if isinstance(request, SettlementRequest) else SettlementRequest.fromObj(request)
204
+ decision = self.canSettle(normalized, req)
205
+ status = "ready" if decision["ok"] else "rejected"
206
+ adapterSpec = adapter if isinstance(adapter, SettlementAdapterSpec) else SettlementAdapterSpec.fromObj(adapter)
207
+ rail = self.rail(normalized)
208
+ railId = str(rail.get("railId", adapterSpec.rail) or adapterSpec.rail)
209
+ payerId = self.payerId(normalized)
210
+ payeeId = self.payeeId(normalized) or req.vendorId
211
+ createdAt = self.clock().isoformat().replace("+00:00", "Z")
212
+ adapterPayload = {
213
+ "idempotencyKey": self._stableId(
214
+ "settlement",
215
+ {
216
+ "mandateId": str(normalized.get("mandateId", "") or ""),
217
+ "taskId": req.taskId,
218
+ "payeeId": payeeId,
219
+ "amount": req.amount.toMap(),
220
+ },
221
+ ),
222
+ "mandateId": str(normalized.get("mandateId", "") or ""),
223
+ "intentMandateId": str(normalized.get("intentMandateId", "") or ""),
224
+ "taskId": req.taskId,
225
+ "payerId": payerId,
226
+ "payeeId": payeeId,
227
+ "amount": req.amount.toMap(),
228
+ "povRef": req.pov.povId,
229
+ }
230
+ base = {
231
+ "status": status,
232
+ "adapterId": adapterSpec.adapterId,
233
+ "rail": railId,
234
+ "network": str(rail.get("network", adapterSpec.network) or adapterSpec.network),
235
+ "mandateId": str(normalized.get("mandateId", "") or ""),
236
+ "intentMandateId": str(normalized.get("intentMandateId", "") or ""),
237
+ "taskId": req.taskId,
238
+ "payerId": payerId,
239
+ "payeeId": payeeId,
240
+ "amount": req.amount.toMap(),
241
+ "povRef": req.pov.povId,
242
+ "escrowId": str((request if isinstance(request, dict) else {}).get("escrowId", "") or ""),
243
+ "errors": decision["errors"],
244
+ "createdAt": createdAt,
245
+ "adapterPayload": adapterPayload,
246
+ }
247
+ settlementId = "setl_" + hashlib.sha256(
248
+ json.dumps(base, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
249
+ ).hexdigest()[:24]
250
+ instruction = {"settlementId": settlementId, **base}
251
+ result = {
252
+ "ok": decision["ok"],
253
+ "decision": decision,
254
+ "instruction": instruction,
255
+ "adapter": adapterSpec.toMap(),
256
+ "contract": self.settlementAdapterContract(),
257
+ }
258
+ adapterRunner = self.adapterRunners.get(adapterSpec.adapterId) or self._adapterForRail(railId)
259
+ if adapterRunner is not None:
260
+ result["adapterExecution"] = adapterRunner.invoke("settlement.prepare", {"instruction": instruction}, self._adapterContext())
261
+ return result
262
+
263
+ @staticmethod
264
+ def settlementAdapterContract() -> dict[str, Any]:
265
+ return {
266
+ "ok": True,
267
+ "contract": "picuxSettlementAdapter",
268
+ "version": "2026-05-10",
269
+ "mode": "prepare|invoke",
270
+ "routes": {
271
+ "contract": "GET /v1/pay/adapters/contract",
272
+ "invoke": "POST /v1/pay/adapters/{adapterId}/invoke",
273
+ },
274
+ "input": {
275
+ "required": ["mandateId", "taskId", "payerId", "payeeId", "amount", "povRef", "idempotencyKey"],
276
+ "optional": ["intentMandateId", "escrowId", "rail", "network", "meta"],
277
+ },
278
+ "output": {
279
+ "required": ["adapterId", "status", "providerRef", "createdAt"],
280
+ "statuses": ["accepted", "rejected", "pending", "settled", "failed"],
281
+ },
282
+ "rules": [
283
+ "Adapters receive no issuer private keys.",
284
+ "Adapters must treat idempotencyKey as replay protection.",
285
+ "Adapters must return a providerRef before Picux records external execution.",
286
+ ],
287
+ }
288
+
289
+ def listRails(self) -> dict[str, Any]:
290
+ rails = self.rails()
291
+ return {"ok": True, "rails": rails, "count": len(rails)}
292
+
293
+ def getRail(self, railId: str) -> dict[str, Any]:
294
+ requested = str(railId or "")
295
+ rail = next((item for item in self.rails() if item["railId"] == requested), None)
296
+ if rail is None:
297
+ return {"ok": False, "error": "railNotFound", "railId": requested}
298
+ return {"ok": True, "rail": rail}
299
+
300
+ def listAdapters(self) -> dict[str, Any]:
301
+ adapters = self._adapterCatalog()
302
+ return {"ok": True, "adapters": adapters, "count": len(adapters)}
303
+
304
+ def getAdapter(self, adapterId: str) -> dict[str, Any]:
305
+ requested = str(adapterId or "")
306
+ adapter = next((item for item in self._adapterCatalog() if item["adapterId"] == requested), None)
307
+ if adapter is None:
308
+ return {"ok": False, "error": "adapterNotFound", "adapterId": requested}
309
+ return {"ok": True, "adapter": adapter}
310
+
311
+ def invokeAdapter(self, adapterId: str, payload: dict[str, Any]) -> dict[str, Any]:
312
+ requested = str(adapterId or "")
313
+ adapter = self.adapterRunners.get(requested)
314
+ if adapter is None:
315
+ return {"ok": False, "error": "adapterNotFound", "adapterId": requested}
316
+ action = str(payload.get("action", "") or self._inferAdapterAction(payload))
317
+ runnerPayload = payload.get("payload", payload) if isinstance(payload.get("payload", payload), dict) else {}
318
+ result = adapter.invoke(action, runnerPayload, self._adapterContext())
319
+ return {"ok": bool(result.get("ok")), "adapter": adapter.metadata(self.env), "result": result}
320
+
321
+ def listReputationCards(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
322
+ filters = filters or {}
323
+ subjectId = str(filters.get("subjectId", "") or "")
324
+ context = str(filters.get("context", "") or "")
325
+ cards = list(self.reputationCards.values())
326
+ if subjectId:
327
+ cards = [card for card in cards if card.get("subjectId") == subjectId]
328
+ if context:
329
+ cards = [card for card in cards if card.get("context") == context]
330
+ cards.sort(key=lambda item: (str(item.get("updatedAt", "")), str(item.get("cardId", ""))), reverse=True)
331
+ return {"ok": True, "cards": [copy.deepcopy(card) for card in cards], "count": len(cards)}
332
+
333
+ def getReputationCard(self, cardId: str) -> dict[str, Any]:
334
+ requested = str(cardId or "")
335
+ card = self.reputationCards.get(requested)
336
+ if card is None:
337
+ return {"ok": False, "error": "reputationCardNotFound", "cardId": requested}
338
+ return {"ok": True, "card": copy.deepcopy(card)}
339
+
340
+ def updateReputationCard(self, payload: dict[str, Any]) -> dict[str, Any]:
341
+ subjectId = str(payload.get("subjectId", "") or "")
342
+ subjectKind = str(payload.get("subjectKind", "agent") or "agent")
343
+ context = str(payload.get("context", "settlementReliability") or "settlementReliability")
344
+ if not subjectId:
345
+ return {"ok": False, "error": "missing:subjectId"}
346
+ cardId = str(payload.get("cardId", "") or self._stableId("rep", {"subjectId": subjectId, "subjectKind": subjectKind, "context": context}))
347
+ current = self.reputationCards.get(cardId, {})
348
+ baseScore = float(current.get("score", payload.get("baseScore", 50.0)) or 50.0)
349
+ delta = float(payload.get("delta", 0.0) or 0.0)
350
+ score = min(100.0, max(0.0, baseScore + delta))
351
+ evidenceRefs = current.get("evidenceRefs", []) if isinstance(current.get("evidenceRefs"), list) else []
352
+ evidenceRef = str(payload.get("evidenceRef", "") or "")
353
+ if evidenceRef and evidenceRef not in evidenceRefs:
354
+ evidenceRefs = [*evidenceRefs, evidenceRef]
355
+ card = {
356
+ "cardId": cardId,
357
+ "subjectId": subjectId,
358
+ "subjectKind": subjectKind,
359
+ "context": context,
360
+ "score": round(score, 2),
361
+ "thresholds": payload.get("thresholds", current.get("thresholds", {}))
362
+ if isinstance(payload.get("thresholds", current.get("thresholds", {})), dict)
363
+ else {},
364
+ "evidenceRefs": evidenceRefs,
365
+ "chainRef": str(payload.get("chainRef", current.get("chainRef", "")) or ""),
366
+ "updatedAt": self._now(),
367
+ "meta": payload.get("meta", current.get("meta", {})) if isinstance(payload.get("meta", current.get("meta", {})), dict) else {},
368
+ }
369
+ self.reputationCards[cardId] = copy.deepcopy(card)
370
+ return {"ok": True, "card": card, "delta": delta}
371
+
372
+ def createPaymentChallenge(self, payload: dict[str, Any]) -> dict[str, Any]:
373
+ now = self._now()
374
+ ttl = self._positiveInt(payload.get("ttlSeconds"), 300)
375
+ amount = Money.fromObj(payload.get("amount", {}) if isinstance(payload.get("amount"), dict) else {})
376
+ if amount.currency == "USD" and payload.get("currency"):
377
+ amount = Money(amount.amount, str(payload.get("currency", "USD") or "USD"))
378
+ if amount.amount <= 0 and payload.get("price"):
379
+ amount = Money.fromObj(payload.get("price", {}) if isinstance(payload.get("price"), dict) else {})
380
+ railId = str(payload.get("railId", payload.get("rail", "picuxLedger")) or "picuxLedger")
381
+ resource = payload.get("resource", {}) if isinstance(payload.get("resource"), dict) else {}
382
+ resourceId = str(
383
+ resource.get(
384
+ "resourceId",
385
+ payload.get("resourceId", payload.get("path", payload.get("connectorId", "resource"))),
386
+ )
387
+ or "resource"
388
+ )
389
+ challengeBase = {
390
+ "railId": railId,
391
+ "resourceId": resourceId,
392
+ "method": str(resource.get("method", payload.get("method", "POST")) or "POST").upper(),
393
+ "path": str(resource.get("path", payload.get("path", "")) or ""),
394
+ "amount": amount.toMap(),
395
+ "payer": payload.get("payer", {}) if isinstance(payload.get("payer"), dict) else {},
396
+ "payee": payload.get("payee", {}) if isinstance(payload.get("payee"), dict) else {},
397
+ "nonce": str(payload.get("nonce", "") or ""),
398
+ }
399
+ challengeId = str(payload.get("challengeId", "") or self._stableId("paychal", challengeBase))
400
+ challenge = {
401
+ "challengeId": challengeId,
402
+ "status": "requiresPayment",
403
+ "railId": railId,
404
+ "resource": {
405
+ "resourceId": resourceId,
406
+ "method": challengeBase["method"],
407
+ "path": challengeBase["path"],
408
+ },
409
+ "amount": amount.toMap(),
410
+ "payer": challengeBase["payer"],
411
+ "payee": challengeBase["payee"],
412
+ "headers": {
413
+ "WWW-Authenticate": f'Payment realm="picux", rail="{railId}", challenge="{challengeId}"',
414
+ "X-Picux-Payment-Challenge": challengeId,
415
+ },
416
+ "credentialReq": {
417
+ "required": ["challengeId", "railId", "payer", "signature"],
418
+ "acceptedRails": [railId],
419
+ "mode": "paymentAuth",
420
+ },
421
+ "retry": {
422
+ "method": challengeBase["method"],
423
+ "path": challengeBase["path"],
424
+ "headers": ["Payment-Signature", "X-Picux-Payment-Credential"],
425
+ },
426
+ "createdAt": now,
427
+ "expiresAt": self._iso(self.clock() + timedelta(seconds=ttl)),
428
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
429
+ }
430
+ adapterResult = self._invokeRailAdapter(railId, "challenge.create", {"challenge": challenge, **payload})
431
+ if adapterResult:
432
+ challenge["adapter"] = adapterResult
433
+ resultHeaders = adapterResult.get("headers", {}) if isinstance(adapterResult.get("headers"), dict) else {}
434
+ challenge["headers"] = {**challenge["headers"], **resultHeaders}
435
+ if payload.get("requireConfiguredRail") and not adapterResult.get("ok"):
436
+ self.challenges[challengeId] = copy.deepcopy(challenge)
437
+ return {"ok": False, "challenge": challenge, "status": "adapterNotConfigured", "adapter": adapterResult}
438
+ self.challenges[challengeId] = copy.deepcopy(challenge)
439
+ return {"ok": True, "challenge": challenge, "status": "requiresPayment"}
440
+
441
+ def verifyPaymentChallenge(self, challengeId: str, payload: dict[str, Any]) -> dict[str, Any]:
442
+ credential = payload.get("credential", payload) if isinstance(payload, dict) else {}
443
+ credential = credential if isinstance(credential, dict) else {}
444
+ challenge = self.challenges.get(str(challengeId or ""), {})
445
+ challengeRail = str(challenge.get("railId", credential.get("railId", "")) or "") if isinstance(challenge, dict) else str(credential.get("railId", "") or "")
446
+ if challengeRail and not credential.get("railId"):
447
+ credential = {**credential, "railId": challengeRail}
448
+ errors: list[str] = []
449
+ if str(credential.get("challengeId", challengeId) or "") != str(challengeId or ""):
450
+ errors.append("challengeMismatch")
451
+ requiredCredentialKeys = ("railId",) if challengeRail in {"x402", "l402"} else ("railId", "payer")
452
+ for key in requiredCredentialKeys:
453
+ if not credential.get(key):
454
+ errors.append(f"missing:{key}")
455
+ credentialRail = str(credential.get("railId", "") or "")
456
+ if credentialRail in {"picuxLedger", "mpp", "stripeSpt", "solanaPay", "baseUsdc"} and not credential.get("signature"):
457
+ errors.append("missing:signature")
458
+ adapterResult: dict[str, Any] | None = None
459
+ if challenge:
460
+ if str(credential.get("railId", "") or "") != challengeRail:
461
+ errors.append("railMismatch")
462
+ adapterResult = self._invokeRailAdapter(
463
+ challengeRail,
464
+ "challenge.verify",
465
+ {
466
+ "challenge": challenge,
467
+ "credential": credential,
468
+ "headers": payload.get("headers", {}) if isinstance(payload.get("headers"), dict) else {},
469
+ },
470
+ )
471
+ if adapterResult is None:
472
+ errors.append("adapterNotFound")
473
+ elif not adapterResult.get("ok"):
474
+ adapterErrors = adapterResult.get("errors", []) if isinstance(adapterResult.get("errors"), list) else []
475
+ errors.extend(adapterErrors or [str(adapterResult.get("error", "adapterRejected") or "adapterRejected")])
476
+ else:
477
+ errors.append("challengeNotFound")
478
+ status = "verified" if not errors else "rejected"
479
+ credentialId = str(credential.get("credentialId", "") or self._stableId("paycred", credential))
480
+ receipt = {
481
+ "receiptId": self._stableId("payrcpt", {"challengeId": challengeId, "credential": credential}),
482
+ "challengeId": str(challengeId or ""),
483
+ "credentialId": credentialId,
484
+ "status": status,
485
+ "railId": str(credential.get("railId", "") or ""),
486
+ "paymentRef": str(credential.get("paymentRef", "") or ""),
487
+ "amount": challenge.get("amount", credential.get("amount", {})) if isinstance(challenge, dict) else {},
488
+ "resource": challenge.get("resource", {}) if isinstance(challenge, dict) else {},
489
+ "adapter": adapterResult if isinstance(adapterResult, dict) else {},
490
+ "createdAt": self._now(),
491
+ }
492
+ if not errors:
493
+ self.verifiedCredentials[credentialId] = copy.deepcopy(receipt)
494
+ return {
495
+ "ok": not errors,
496
+ "challengeId": str(challengeId or ""),
497
+ "status": status,
498
+ "errors": errors,
499
+ "receipt": receipt,
500
+ }
501
+
502
+ def createPaymentSession(self, payload: dict[str, Any]) -> dict[str, Any]:
503
+ session = self._session(payload, status="created")
504
+ self.sessions[session["sessionId"]] = copy.deepcopy(session)
505
+ return {"ok": True, "session": session}
506
+
507
+ def authorizePaymentSession(self, sessionId: str, payload: dict[str, Any]) -> dict[str, Any]:
508
+ session = self._session({**self.sessions.get(str(sessionId or ""), {}), **payload, "sessionId": sessionId}, status="authorized")
509
+ self.sessions[session["sessionId"]] = copy.deepcopy(session)
510
+ return {"ok": True, "session": session, "status": "authorized"}
511
+
512
+ def consumePaymentSession(self, sessionId: str, payload: dict[str, Any]) -> dict[str, Any]:
513
+ existing = self.sessions.get(str(sessionId or ""), {})
514
+ errors: list[str] = []
515
+ if not existing:
516
+ errors.append("sessionNotFound")
517
+ if existing and str(existing.get("status", "") or "") not in {"authorized", "active"}:
518
+ errors.append("sessionNotAuthorized")
519
+ session = self._session({**existing, **payload, "sessionId": sessionId}, status="active")
520
+ amount = Money.fromObj(payload.get("amount", {}) if isinstance(payload.get("amount"), dict) else {})
521
+ previous = Money.fromObj(existing.get("spent", {}) if isinstance(existing.get("spent"), dict) else {})
522
+ spent = Money(previous.amount + amount.amount, amount.currency or previous.currency)
523
+ limit = Money.fromObj(session.get("sessionLimit", {}) if isinstance(session.get("sessionLimit"), dict) else {})
524
+ cap = Money.fromObj(session.get("perRequestCap", {}) if isinstance(session.get("perRequestCap"), dict) else {})
525
+ resourceId = self._resourceId(payload.get("resource", payload.get("resourceId", "")))
526
+ allowedResources = session.get("allowedResources", []) if isinstance(session.get("allowedResources"), list) else []
527
+ if amount.amount <= 0:
528
+ errors.append("invalid:amount")
529
+ if cap.amount > 0 and (amount.currency != cap.currency or amount.amount > cap.amount):
530
+ errors.append("perRequestCapExceeded")
531
+ if allowedResources and resourceId not in allowedResources:
532
+ errors.append("resourceNotAllowed")
533
+ if spent.currency != limit.currency or spent.amount > limit.amount:
534
+ errors.append("sessionLimitExceeded")
535
+ ok = not errors
536
+ session["spent"] = spent.toMap() if ok else previous.toMap()
537
+ if not ok and "sessionLimitExceeded" in errors:
538
+ session["status"] = "exhausted"
539
+ elif not ok:
540
+ session["status"] = "rejected"
541
+ usage = {
542
+ "usageId": self._stableId("payuse", {"sessionId": sessionId, "payload": payload}),
543
+ "sessionId": str(sessionId or ""),
544
+ "amount": amount.toMap(),
545
+ "resource": payload.get("resource", {}) if isinstance(payload.get("resource"), dict) else {"resourceId": resourceId},
546
+ "createdAt": self._now(),
547
+ }
548
+ self.sessions[session["sessionId"]] = copy.deepcopy(session)
549
+ return {"ok": ok, "session": session, "usage": usage, "status": "recorded" if ok else "rejected", "errors": errors}
550
+
551
+ def closePaymentSession(self, sessionId: str, payload: dict[str, Any]) -> dict[str, Any]:
552
+ session = self._session({**self.sessions.get(str(sessionId or ""), {}), **payload, "sessionId": sessionId}, status="closed")
553
+ self.sessions[session["sessionId"]] = copy.deepcopy(session)
554
+ return {"ok": True, "session": session, "status": "closed"}
555
+
556
+ def localPaymentCredential(self, challenge: dict[str, Any], payer: dict[str, Any] | None = None) -> dict[str, Any]:
557
+ payerObj = payer if isinstance(payer, dict) else challenge.get("payer", {})
558
+ payerObj = payerObj if isinstance(payerObj, dict) else {}
559
+ challengeId = str(challenge.get("challengeId", "") or "")
560
+ railId = str(challenge.get("railId", "picuxLedger") or "picuxLedger")
561
+ createdAt = self._now()
562
+ credential = {
563
+ "credentialId": self._stableId("paycred", {"challengeId": challengeId, "railId": railId, "payer": payerObj}),
564
+ "challengeId": challengeId,
565
+ "railId": railId,
566
+ "payer": payerObj,
567
+ "signature": self.localPaymentSignature(challengeId=challengeId, railId=railId, payer=payerObj),
568
+ "paymentRef": self._stableId("localpay", {"challengeId": challengeId, "payer": payerObj}),
569
+ "amount": challenge.get("amount", {}) if isinstance(challenge.get("amount"), dict) else {},
570
+ "createdAt": createdAt,
571
+ "meta": {"mode": "picuxLedgerLocal"},
572
+ }
573
+ return credential
574
+
575
+ def authorizePaymentHeaders(self, headers: dict[str, Any]) -> dict[str, Any]:
576
+ normalized = {str(key).lower(): str(value) for key, value in headers.items()}
577
+ challengeId = normalized.get("x-picux-payment-challenge", "")
578
+ credentialId = normalized.get("x-picux-payment-credential", "")
579
+ signature = normalized.get("payment-signature", "")
580
+ receipt = self.verifiedCredentials.get(credentialId, {})
581
+ errors: list[str] = []
582
+ if not challengeId:
583
+ errors.append("missing:x-picux-payment-challenge")
584
+ if not credentialId:
585
+ errors.append("missing:x-picux-payment-credential")
586
+ if not signature:
587
+ errors.append("missing:payment-signature")
588
+ if not receipt:
589
+ errors.append("credentialNotVerified")
590
+ if receipt and str(receipt.get("challengeId", "") or "") != challengeId:
591
+ errors.append("challengeMismatch")
592
+ return {
593
+ "ok": not errors,
594
+ "errors": errors,
595
+ "challengeId": challengeId,
596
+ "credentialId": credentialId,
597
+ "receipt": receipt,
598
+ "status": "authorized" if not errors else "paymentRequired",
599
+ }
600
+
601
+ @classmethod
602
+ def localPaymentSignature(cls, *, challengeId: str, railId: str, payer: dict[str, Any]) -> str:
603
+ return cls._stableId("paysig", {"challengeId": challengeId, "railId": railId, "payer": payer})
604
+
605
+ @staticmethod
606
+ def rails() -> list[dict[str, Any]]:
607
+ return [
608
+ {
609
+ "railId": "picuxLedger",
610
+ "kind": "ledger",
611
+ "status": "local",
612
+ "network": "local",
613
+ "asset": "USD",
614
+ "settlement": {"mode": "adapter"},
615
+ "auth": {"mode": "payment"},
616
+ "limits": {"minAmount": 0, "supportsSessions": True},
617
+ "adapter": {"adapterId": "picuxLedgerAdapter"},
618
+ "meta": {"desc": "Deterministic local rail for contract tests and sandbox runs."},
619
+ },
620
+ {
621
+ "railId": "x402",
622
+ "kind": "x402",
623
+ "status": "contract",
624
+ "network": "base",
625
+ "asset": "USDC",
626
+ "settlement": {"mode": "external"},
627
+ "auth": {"mode": "payment"},
628
+ "limits": {"supportsSessions": False},
629
+ "adapter": {"adapterId": "x402Verifier"},
630
+ "meta": {"desc": "HTTP 402 challenge/retry payment rail."},
631
+ },
632
+ {
633
+ "railId": "mpp",
634
+ "kind": "mpp",
635
+ "status": "contract",
636
+ "network": "http",
637
+ "asset": "USD",
638
+ "settlement": {"mode": "session"},
639
+ "auth": {"mode": "session"},
640
+ "limits": {"supportsSessions": True},
641
+ "adapter": {"adapterId": "mppSessionAdapter"},
642
+ "meta": {"desc": "Machine-payment session rail for high-frequency calls."},
643
+ },
644
+ {
645
+ "railId": "l402",
646
+ "kind": "l402",
647
+ "status": "planned",
648
+ "network": "lightning",
649
+ "asset": "BTC",
650
+ "settlement": {"mode": "external"},
651
+ "auth": {"mode": "macaroon"},
652
+ "limits": {"supportsSessions": False},
653
+ "adapter": {"adapterId": "l402Adapter"},
654
+ "meta": {"desc": "Macaroon and invoice-based payment authentication."},
655
+ },
656
+ {
657
+ "railId": "stripeSpt",
658
+ "kind": "stripeSpt",
659
+ "status": "contract",
660
+ "network": "stripe",
661
+ "asset": "fiat",
662
+ "settlement": {"mode": "external"},
663
+ "auth": {"mode": "token"},
664
+ "limits": {"supportsSessions": True},
665
+ "adapter": {"adapterId": "stripeSptAdapter"},
666
+ "meta": {"desc": "Fiat/card compatibility for agentic commerce."},
667
+ },
668
+ {
669
+ "railId": "solanaPay",
670
+ "kind": "solanaPay",
671
+ "status": "planned",
672
+ "network": "solana",
673
+ "asset": "USDC",
674
+ "settlement": {"mode": "escrow"},
675
+ "auth": {"mode": "mandate"},
676
+ "limits": {"supportsSessions": False},
677
+ "adapter": {"adapterId": "solanaEscrowAdapter"},
678
+ "meta": {"desc": "Proof-of-utility escrow rail for on-chain settlement."},
679
+ },
680
+ {
681
+ "railId": "baseUsdc",
682
+ "kind": "baseUsdc",
683
+ "status": "planned",
684
+ "network": "base",
685
+ "asset": "USDC",
686
+ "settlement": {"mode": "escrow"},
687
+ "auth": {"mode": "mandate"},
688
+ "limits": {"supportsSessions": False},
689
+ "adapter": {"adapterId": "baseEscrowAdapter"},
690
+ "meta": {"desc": "Base USDC escrow settlement rail."},
691
+ },
692
+ ]
693
+
694
+ @staticmethod
695
+ def adapters() -> list[dict[str, Any]]:
696
+ return [
697
+ {
698
+ "adapterId": "picuxLedgerAdapter",
699
+ "railId": "picuxLedger",
700
+ "status": "local",
701
+ "mode": "verify",
702
+ "capabilities": ["challenge.verify", "session.consume", "escrow.test"],
703
+ "requiredEnv": [],
704
+ "liveExecution": False,
705
+ },
706
+ {
707
+ "adapterId": "x402Verifier",
708
+ "railId": "x402",
709
+ "status": "contract",
710
+ "mode": "verify",
711
+ "capabilities": ["challenge.create", "challenge.verify", "facilitator.prepare"],
712
+ "requiredEnv": ["PICUX_X402_FACILITATOR_URL"],
713
+ "liveExecution": False,
714
+ },
715
+ {
716
+ "adapterId": "mppSessionAdapter",
717
+ "railId": "mpp",
718
+ "status": "contract",
719
+ "mode": "session",
720
+ "capabilities": ["session.authorize", "session.consume", "session.close"],
721
+ "requiredEnv": ["PICUX_MPP_ISSUER"],
722
+ "liveExecution": False,
723
+ },
724
+ {
725
+ "adapterId": "stripeSptAdapter",
726
+ "railId": "stripeSpt",
727
+ "status": "contract",
728
+ "mode": "execute",
729
+ "capabilities": ["token.prepare", "fiat.authorize", "fiat.capture"],
730
+ "requiredEnv": ["STRIPE_SECRET_KEY"],
731
+ "liveExecution": False,
732
+ },
733
+ {
734
+ "adapterId": "solanaEscrowAdapter",
735
+ "railId": "solanaPay",
736
+ "status": "planned",
737
+ "mode": "escrow",
738
+ "capabilities": ["mission.initialize", "mission.settle", "mission.abort"],
739
+ "requiredEnv": ["PICUX_SOLANA_RPC_URL", "PICUX_SOLANA_PROGRAM_ID"],
740
+ "liveExecution": False,
741
+ },
742
+ {
743
+ "adapterId": "baseEscrowAdapter",
744
+ "railId": "baseUsdc",
745
+ "status": "planned",
746
+ "mode": "escrow",
747
+ "capabilities": ["mission.initialize", "mission.settle", "mission.abort"],
748
+ "requiredEnv": ["PICUX_BASE_RPC_URL", "PICUX_BASE_ESCROW_ADDRESS"],
749
+ "liveExecution": False,
750
+ },
751
+ {
752
+ "adapterId": "l402Adapter",
753
+ "railId": "l402",
754
+ "status": "planned",
755
+ "mode": "verify",
756
+ "capabilities": ["macaroon.issue", "invoice.verify", "preimage.redeem"],
757
+ "requiredEnv": ["PICUX_L402_NODE_URL"],
758
+ "liveExecution": False,
759
+ },
760
+ ]
761
+
762
+ def _adapterCatalog(self) -> list[dict[str, Any]]:
763
+ return [adapter.metadata(self.env) for adapter in self.adapterRunners.values()]
764
+
765
+ def _adapterContext(self) -> PayRailAdapterContext:
766
+ return PayRailAdapterContext(env=self.env, createdAt=self._now())
767
+
768
+ def _adapterForRail(self, railId: str) -> PayRailAdapter | None:
769
+ requested = str(railId or "")
770
+ return next((adapter for adapter in self.adapterRunners.values() if adapter.railId == requested), None)
771
+
772
+ def _invokeRailAdapter(self, railId: str, action: str, payload: dict[str, Any]) -> dict[str, Any] | None:
773
+ adapter = self._adapterForRail(railId)
774
+ if adapter is None:
775
+ return None
776
+ return adapter.invoke(action, payload, self._adapterContext())
777
+
778
+ @staticmethod
779
+ def _inferAdapterAction(payload: dict[str, Any]) -> str:
780
+ if isinstance(payload.get("instruction"), dict):
781
+ return "settlement.prepare"
782
+ if isinstance(payload.get("challenge"), dict):
783
+ return "challenge.create"
784
+ if isinstance(payload.get("session"), dict):
785
+ return "session.authorize"
786
+ return "settlement.prepare"
787
+
788
+ def freeze(self, *, mandateId: str, taskId: str, reason: str) -> dict[str, Any]:
789
+ return self.receipt(
790
+ mandateId=mandateId,
791
+ taskId=taskId,
792
+ status="frozen",
793
+ amount=Money(0.0, "USD"),
794
+ pov=ProofOfValue(""),
795
+ errors=[reason],
796
+ )
797
+
798
+ def receipt(
799
+ self,
800
+ *,
801
+ mandateId: str,
802
+ taskId: str,
803
+ status: str,
804
+ amount: Money,
805
+ pov: ProofOfValue,
806
+ errors: list[str] | None = None,
807
+ ) -> dict[str, Any]:
808
+ createdAt = self.clock().isoformat().replace("+00:00", "Z")
809
+ base = {
810
+ "mandateId": mandateId,
811
+ "taskId": taskId,
812
+ "status": status,
813
+ "amount": amount.toMap()["amount"],
814
+ "currency": amount.currency,
815
+ "povRef": pov.povId,
816
+ "errors": errors or [],
817
+ "createdAt": createdAt,
818
+ }
819
+ receiptId = "rcpt_" + hashlib.sha256(
820
+ json.dumps(base, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
821
+ ).hexdigest()[:24]
822
+ return {"receiptId": receiptId, **base}
823
+
824
+ def canonicalPayload(self, mandate: dict[str, Any]) -> dict[str, Any]:
825
+ payload = copy.deepcopy(mandate)
826
+ payload.pop("signature", None)
827
+ return payload
828
+
829
+ @staticmethod
830
+ def mandateKind(mandate: dict[str, Any]) -> str:
831
+ kind = str(mandate.get("kind", mandate.get("type", "")) or "").strip().lower()
832
+ return "payment" if kind == "payment" else "intent"
833
+
834
+ @staticmethod
835
+ def allowedVendors(mandate: dict[str, Any]) -> list[str]:
836
+ constraints = mandate.get("constraints", {}) if isinstance(mandate.get("constraints"), dict) else {}
837
+ allowed = list(constraints.get("allowedVendors", [])) + list(constraints.get("allowedPayees", []))
838
+ payeeId = PayDomain.payeeId(mandate)
839
+ if payeeId:
840
+ allowed.append(payeeId)
841
+ return [str(item) for item in allowed if str(item).strip()]
842
+
843
+ @staticmethod
844
+ def resolveRules(mandate: dict[str, Any]) -> list[str]:
845
+ constraints = mandate.get("constraints", {}) if isinstance(mandate.get("constraints"), dict) else {}
846
+ return [str(item) for item in constraints.get("resolveRules", []) if str(item).strip()]
847
+
848
+ @staticmethod
849
+ def payerId(mandate: dict[str, Any]) -> str:
850
+ payer = mandate.get("payer", {}) if isinstance(mandate.get("payer"), dict) else {}
851
+ return str(payer.get("entityId", mandate.get("payerId", "")) or "")
852
+
853
+ @staticmethod
854
+ def payeeId(mandate: dict[str, Any]) -> str:
855
+ payee = mandate.get("payee", {}) if isinstance(mandate.get("payee"), dict) else {}
856
+ return str(payee.get("entityId", mandate.get("payeeId", "")) or "")
857
+
858
+ @staticmethod
859
+ def rail(mandate: dict[str, Any]) -> dict[str, Any]:
860
+ rail = mandate.get("rail", {}) if isinstance(mandate.get("rail"), dict) else {}
861
+ if not rail and mandate.get("rail"):
862
+ rail = {"railId": str(mandate.get("rail", "") or "")}
863
+ return {
864
+ "railId": str(rail.get("railId", rail.get("id", rail.get("type", ""))) or ""),
865
+ "network": str(rail.get("network", "") or ""),
866
+ "asset": str(rail.get("asset", rail.get("currency", "")) or ""),
867
+ "adapterId": str(rail.get("adapterId", "") or ""),
868
+ }
869
+
870
+ @staticmethod
871
+ def _constraintMaxSpend(mandate: dict[str, Any]) -> Money:
872
+ constraints = mandate.get("constraints", {}) if isinstance(mandate.get("constraints"), dict) else {}
873
+ maxSpend = constraints.get("maxSpend", {}) if isinstance(constraints.get("maxSpend"), dict) else {}
874
+ return Money.fromObj(maxSpend)
875
+
876
+ @staticmethod
877
+ def _paymentAmount(mandate: dict[str, Any]) -> Money:
878
+ return Money.fromObj(mandate.get("amount", {}) if isinstance(mandate.get("amount"), dict) else {})
879
+
880
+ @staticmethod
881
+ def _normalizeMandate(mandate: dict[str, Any]) -> dict[str, Any]:
882
+ return mandate if isinstance(mandate, dict) else {}
883
+
884
+ @staticmethod
885
+ def _stableId(prefix: str, payload: dict[str, Any]) -> str:
886
+ digest = hashlib.sha256(
887
+ json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
888
+ ).hexdigest()[:24]
889
+ return f"{prefix}_{digest}"
890
+
891
+ def _session(self, payload: dict[str, Any], *, status: str) -> dict[str, Any]:
892
+ now = self._now()
893
+ ttl = self._positiveInt(payload.get("ttlSeconds"), 3600)
894
+ limit = Money.fromObj(
895
+ payload.get("sessionLimit", payload.get("limit", {})) if isinstance(payload.get("sessionLimit", payload.get("limit", {})), dict) else {}
896
+ )
897
+ spent = Money.fromObj(payload.get("spent", {}) if isinstance(payload.get("spent"), dict) else {})
898
+ if spent.currency == "USD" and limit.currency:
899
+ spent = Money(spent.amount, limit.currency)
900
+ sessionId = str(payload.get("sessionId", "") or self._stableId("paysess", payload))
901
+ payer = payload.get("payer", {}) if isinstance(payload.get("payer"), dict) else {}
902
+ return {
903
+ "sessionId": sessionId,
904
+ "status": status,
905
+ "payer": payer,
906
+ "railId": str(payload.get("railId", payload.get("rail", "picuxLedger")) or "picuxLedger"),
907
+ "sessionLimit": limit.toMap(),
908
+ "perRequestCap": payload.get("perRequestCap", {}) if isinstance(payload.get("perRequestCap"), dict) else {},
909
+ "spent": spent.toMap(),
910
+ "allowedResources": [str(item) for item in payload.get("allowedResources", []) if str(item).strip()]
911
+ if isinstance(payload.get("allowedResources"), list)
912
+ else [],
913
+ "createdAt": now,
914
+ "expiresAt": self._iso(self.clock() + timedelta(seconds=ttl)),
915
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
916
+ }
917
+
918
+ @staticmethod
919
+ def _parseTime(value: str) -> datetime | None:
920
+ raw = str(value or "").strip()
921
+ if raw.endswith("Z"):
922
+ raw = raw[:-1] + "+00:00"
923
+ try:
924
+ parsed = datetime.fromisoformat(raw)
925
+ except Exception:
926
+ return None
927
+ if parsed.tzinfo is None:
928
+ parsed = parsed.replace(tzinfo=timezone.utc)
929
+ return parsed.astimezone(timezone.utc)
930
+
931
+ def _now(self) -> str:
932
+ return self._iso(self.clock())
933
+
934
+ @staticmethod
935
+ def _iso(value: datetime) -> str:
936
+ return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
937
+
938
+ @staticmethod
939
+ def _positiveInt(value: Any, default: int) -> int:
940
+ try:
941
+ parsed = int(value)
942
+ except Exception:
943
+ return default
944
+ return parsed if parsed > 0 else default
945
+
946
+ @staticmethod
947
+ def _resourceId(value: Any) -> str:
948
+ if isinstance(value, dict):
949
+ return str(value.get("resourceId", value.get("id", "")) or "")
950
+ return str(value or "")