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,411 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import time
7
+ from typing import Any
8
+
9
+
10
+ CONNECTOR_KINDS = {"api", "mcp", "a2a", "webhook", "database", "erp", "custom"}
11
+ CONNECTOR_STATUSES = {"active", "paused", "revoked"}
12
+ CONNECTION_SESSION_STATUSES = {"active", "revoked", "expired"}
13
+ CONNECTION_SESSION_MODES = {"clientHostedAdapter", "picuxHostedAdapter", "mcpTool", "webhookCallback"}
14
+ SECRET_KEYS = {"token", "apiKey", "secret", "password", "privateKey", "clientSecret", "value"}
15
+
16
+
17
+ class ConnectorBook:
18
+ """Bridge connector registry with scoped credential references."""
19
+
20
+ def __init__(self, *, backing: Any | None = None) -> None:
21
+ self.backing = backing
22
+ self._records: dict[str, dict[str, Any]] = {}
23
+ self._sessions: dict[str, dict[str, Any]] = {}
24
+
25
+ def saveConnector(self, payload: dict[str, Any], *, status: str = "active") -> dict[str, Any]:
26
+ connectorId = str(payload.get("connectorId", payload.get("id", "")) or "")
27
+ if not connectorId:
28
+ return {"ok": False, "error": "missing:connectorId"}
29
+ errors = _connectorErrors(payload)
30
+ if errors:
31
+ return {"ok": False, "error": "invalid:connector", "errors": errors}
32
+ current = self.getConnector(connectorId)
33
+ now = int(time.time())
34
+ createdAt = current.get("connector", {}).get("createdAt", now) if current.get("ok") else now
35
+ record = _recordFromPayload({**payload, "connectorId": connectorId}, status=status, createdAt=createdAt, updatedAt=now)
36
+ self._saveRecord(record)
37
+ return {"ok": True, "connector": copy.deepcopy(record)}
38
+
39
+ def getConnector(self, connectorId: str) -> dict[str, Any]:
40
+ connectorId = str(connectorId or "")
41
+ record = self._records.get(connectorId)
42
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchConnector"):
43
+ record = self.backing.fetchConnector(connectorId)
44
+ if record:
45
+ self._records[connectorId] = copy.deepcopy(record)
46
+ if not record:
47
+ return {"ok": False, "error": "connectorNotFound", "connectorId": connectorId}
48
+ return {"ok": True, "connector": copy.deepcopy(record)}
49
+
50
+ def listConnectors(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
51
+ self._loadBacking()
52
+ filters = filters or {}
53
+ records = self._filtered(filters)
54
+ limit = _limit(filters.get("limit", 100))
55
+ return {"ok": True, "connectors": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
56
+
57
+ def matchConnectors(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
58
+ filters = filters or {}
59
+ records = self._filtered({**filters, "status": str(filters.get("status", "active") or "active")})
60
+ return {"ok": True, "connectors": [copy.deepcopy(record) for record in records], "count": len(records)}
61
+
62
+ def updateConnector(self, connectorId: str, updates: dict[str, Any]) -> dict[str, Any]:
63
+ current = self.getConnector(connectorId)
64
+ if not current.get("ok"):
65
+ return current
66
+ merged = {**current["connector"], **updates, "connectorId": connectorId}
67
+ errors = _connectorErrors(merged)
68
+ if errors:
69
+ return {"ok": False, "error": "invalid:connector", "errors": errors}
70
+ record = _recordFromPayload(
71
+ merged,
72
+ status=_status(merged.get("status", current["connector"].get("status", "active"))),
73
+ createdAt=int(current["connector"].get("createdAt", int(time.time())) or int(time.time())),
74
+ updatedAt=int(updates.get("updatedAt", int(time.time())) or int(time.time())),
75
+ )
76
+ self._saveRecord(record)
77
+ return {"ok": True, "connector": copy.deepcopy(record)}
78
+
79
+ def createConnectionSession(self, payload: dict[str, Any]) -> dict[str, Any]:
80
+ errors = _connectionSessionErrors(payload)
81
+ if errors:
82
+ return {"ok": False, "error": "invalid:connectionSession", "errors": errors}
83
+ connectorId = str(payload.get("connectorId", "") or "")
84
+ if connectorId and not self.getConnector(connectorId).get("ok"):
85
+ return {"ok": False, "error": "connectorNotFound", "connectorId": connectorId}
86
+ now = int(time.time())
87
+ sessionId = str(payload.get("sessionId", "") or _sessionId(payload, time.time_ns()))
88
+ record = _connectionSessionFromPayload({**payload, "sessionId": sessionId}, createdAt=now, updatedAt=now)
89
+ self._saveSession(record)
90
+ return {"ok": True, "session": copy.deepcopy(record)}
91
+
92
+ def getConnectionSession(self, sessionId: str) -> dict[str, Any]:
93
+ sessionId = str(sessionId or "")
94
+ record = self._sessions.get(sessionId)
95
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchConnectionSession"):
96
+ record = self.backing.fetchConnectionSession(sessionId)
97
+ if record:
98
+ self._sessions[sessionId] = copy.deepcopy(record)
99
+ if not record:
100
+ return {"ok": False, "error": "connectionSessionNotFound", "sessionId": sessionId}
101
+ return {"ok": True, "session": copy.deepcopy(record)}
102
+
103
+ def listConnectionSessions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
104
+ self._loadBackingSessions()
105
+ filters = filters or {}
106
+ connectorId = str(filters.get("connectorId", "") or "")
107
+ status = str(filters.get("status", "") or "")
108
+ records = list(self._sessions.values())
109
+ if connectorId:
110
+ records = [record for record in records if record.get("connectorId") == connectorId]
111
+ if status:
112
+ records = [record for record in records if record.get("status") == status]
113
+ records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("sessionId", ""))), reverse=True)
114
+ limit = _limit(filters.get("limit", 100))
115
+ return {"ok": True, "sessions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
116
+
117
+ def revokeConnectionSession(self, sessionId: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
118
+ current = self.getConnectionSession(sessionId)
119
+ if not current.get("ok"):
120
+ return current
121
+ payload = payload or {}
122
+ record = {
123
+ **current["session"],
124
+ "status": "revoked",
125
+ "revokedReason": str(payload.get("reason", payload.get("revokedReason", "")) or ""),
126
+ "updatedAt": int(time.time()),
127
+ }
128
+ self._saveSession(record)
129
+ return {"ok": True, "session": copy.deepcopy(record)}
130
+
131
+ def _filtered(self, filters: dict[str, Any]) -> list[dict[str, Any]]:
132
+ self._loadBacking()
133
+ status = str(filters.get("status", "") or "")
134
+ kind = str(filters.get("kind", "") or "")
135
+ domain = str(filters.get("domain", "") or "")
136
+ cap = str(filters.get("cap", filters.get("capability", "")) or "")
137
+ caps = _cleanList(filters.get("caps", []))
138
+ action = str(filters.get("action", "") or "")
139
+ resource = str(filters.get("resource", "") or "")
140
+ records = list(self._records.values())
141
+ if status:
142
+ records = [record for record in records if record.get("status") == status]
143
+ if kind:
144
+ records = [record for record in records if record.get("kind") == kind]
145
+ if domain:
146
+ records = [record for record in records if record.get("domain") == domain]
147
+ if cap:
148
+ records = [record for record in records if cap in record.get("caps", [])]
149
+ if caps:
150
+ records = [record for record in records if set(caps).intersection(record.get("caps", []))]
151
+ if action:
152
+ records = [record for record in records if _hasScope(record, action=action, resource=resource)]
153
+ records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("connectorId", ""))), reverse=True)
154
+ return records
155
+
156
+ def _saveRecord(self, record: dict[str, Any]) -> None:
157
+ connectorId = str(record.get("connectorId", "") or "")
158
+ if not connectorId:
159
+ return
160
+ self._records[connectorId] = copy.deepcopy(record)
161
+ if self._backingEnabled() and hasattr(self.backing, "upsertConnector"):
162
+ self.backing.upsertConnector(record)
163
+
164
+ def _saveSession(self, record: dict[str, Any]) -> None:
165
+ sessionId = str(record.get("sessionId", "") or "")
166
+ if not sessionId:
167
+ return
168
+ self._sessions[sessionId] = copy.deepcopy(record)
169
+ if self._backingEnabled() and hasattr(self.backing, "upsertConnectionSession"):
170
+ self.backing.upsertConnectionSession(record)
171
+
172
+ def _loadBacking(self) -> None:
173
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listConnectors"):
174
+ return
175
+ for record in self.backing.listConnectors():
176
+ connectorId = str(record.get("connectorId", "") or "")
177
+ if connectorId:
178
+ self._records[connectorId] = copy.deepcopy(record)
179
+
180
+ def _loadBackingSessions(self) -> None:
181
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listConnectionSessions"):
182
+ return
183
+ for record in self.backing.listConnectionSessions():
184
+ sessionId = str(record.get("sessionId", "") or "")
185
+ if sessionId:
186
+ self._sessions[sessionId] = copy.deepcopy(record)
187
+
188
+ def _backingEnabled(self) -> bool:
189
+ if self.backing is None:
190
+ return False
191
+ return bool(getattr(self.backing, "enabled", True))
192
+
193
+
194
+ def _recordFromPayload(payload: dict[str, Any], *, status: str, createdAt: int, updatedAt: int) -> dict[str, Any]:
195
+ return {
196
+ "connectorId": str(payload.get("connectorId", "") or ""),
197
+ "status": _status(payload.get("status", status)),
198
+ "domain": str(payload.get("domain", "bridge") or "bridge"),
199
+ "kind": _kind(payload.get("kind", "api")),
200
+ "name": str(payload.get("name", payload.get("connectorId", "")) or ""),
201
+ "endpoint": str(payload.get("endpoint", payload.get("url", "")) or ""),
202
+ "caps": _cleanList(payload.get("caps", payload.get("capabilities", []))),
203
+ "credential": _credential(payload.get("credential", payload.get("auth", {}))),
204
+ "scopes": [_scope(item) for item in payload.get("scopes", []) if isinstance(item, dict)],
205
+ "payment": _payment(payload.get("payment", payload.get("pricing", {}))),
206
+ "health": payload.get("health", {}) if isinstance(payload.get("health"), dict) else {},
207
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
208
+ "createdAt": int(createdAt or updatedAt),
209
+ "updatedAt": int(updatedAt or createdAt),
210
+ }
211
+
212
+
213
+ def _connectionSessionFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
214
+ return {
215
+ "sessionId": str(payload.get("sessionId", "") or ""),
216
+ "connectorId": str(payload.get("connectorId", "") or ""),
217
+ "status": _connectionSessionStatus(payload.get("status", "active")),
218
+ "executionMode": _connectionSessionMode(payload.get("executionMode", "clientHostedAdapter")),
219
+ "subjectId": str(payload.get("subjectId", "") or ""),
220
+ "credentialRefs": _credentialRefs(payload.get("credentialRefs", [])),
221
+ "scopes": [_scope(item) for item in payload.get("scopes", []) if isinstance(item, dict)],
222
+ "callback": _callback(payload.get("callback", {})),
223
+ "providerMeta": payload.get("providerMeta", {}) if isinstance(payload.get("providerMeta"), dict) else {},
224
+ "expiresAt": str(payload.get("expiresAt", "") or ""),
225
+ "createdAt": int(createdAt),
226
+ "updatedAt": int(updatedAt),
227
+ }
228
+
229
+
230
+ def _connectorErrors(payload: dict[str, Any]) -> list[str]:
231
+ errors: list[str] = []
232
+ if _kind(payload.get("kind", "api")) not in CONNECTOR_KINDS:
233
+ errors.append("invalid:kind")
234
+ if not str(payload.get("endpoint", payload.get("url", "")) or "").strip():
235
+ errors.append("missing:endpoint")
236
+ if _hasRawSecret(payload.get("auth", {})) or _hasRawSecret(payload.get("credential", {})):
237
+ errors.append("rawSecretNotAllowed")
238
+ scopes = payload.get("scopes", [])
239
+ if not isinstance(scopes, list):
240
+ errors.append("invalid:scopes")
241
+ else:
242
+ for idx, scope in enumerate(scopes):
243
+ if not isinstance(scope, dict):
244
+ errors.append(f"invalid:scopes.{idx}")
245
+ continue
246
+ if not str(scope.get("scopeId", "") or "").strip():
247
+ errors.append(f"missing:scopes.{idx}.scopeId")
248
+ if not _cleanList(scope.get("actions", [])):
249
+ errors.append(f"missing:scopes.{idx}.actions")
250
+ return errors
251
+
252
+
253
+ def _connectionSessionErrors(payload: dict[str, Any]) -> list[str]:
254
+ errors: list[str] = []
255
+ if not str(payload.get("connectorId", "") or "").strip():
256
+ errors.append("missing:connectorId")
257
+ if _hasRawSecret(payload.get("credential", {})) or _hasRawSecret(payload.get("auth", {})):
258
+ errors.append("rawSecretNotAllowed")
259
+ if not _credentialRefs(payload.get("credentialRefs", [])):
260
+ errors.append("missing:credentialRefs")
261
+ if any(_hasRawSecret(item) for item in payload.get("credentialRefs", []) if isinstance(item, dict)):
262
+ errors.append("rawSecretNotAllowed")
263
+ scopes = payload.get("scopes", [])
264
+ if not isinstance(scopes, list):
265
+ errors.append("invalid:scopes")
266
+ else:
267
+ for idx, scope in enumerate(scopes):
268
+ if not isinstance(scope, dict):
269
+ errors.append(f"invalid:scopes.{idx}")
270
+ continue
271
+ if not str(scope.get("scopeId", "") or "").strip():
272
+ errors.append(f"missing:scopes.{idx}.scopeId")
273
+ if not _cleanList(scope.get("actions", [])):
274
+ errors.append(f"missing:scopes.{idx}.actions")
275
+ return errors
276
+
277
+
278
+ def _credential(value: Any) -> dict[str, Any]:
279
+ raw = value if isinstance(value, dict) else {}
280
+ return {
281
+ "type": str(raw.get("type", "bearer") or "bearer"),
282
+ "tokenRef": str(raw.get("tokenRef", raw.get("secretRef", "")) or ""),
283
+ "env": str(raw.get("env", "") or ""),
284
+ "rotation": str(raw.get("rotation", "") or ""),
285
+ }
286
+
287
+
288
+ def _payment(value: Any) -> dict[str, Any]:
289
+ raw = value if isinstance(value, dict) else {}
290
+ price = raw.get("price", raw.get("amount", {}))
291
+ price = price if isinstance(price, dict) else {}
292
+ acceptedRails = raw.get("acceptedRails", raw.get("rails", []))
293
+ return {
294
+ "required": bool(raw.get("required", False)),
295
+ "pricingModel": str(raw.get("pricingModel", "free") or "free"),
296
+ "price": {
297
+ "amount": float(price.get("amount", 0.0) or 0.0),
298
+ "currency": str(price.get("currency", "USD") or "USD").upper(),
299
+ },
300
+ "acceptedRails": _cleanList(acceptedRails),
301
+ "requiresEscrow": bool(raw.get("requiresEscrow", False)),
302
+ "minTrustScore": float(raw.get("minTrustScore", 0.0) or 0.0),
303
+ "proofReq": _cleanList(raw.get("proofReq", [])),
304
+ "refundPolicy": str(raw.get("refundPolicy", "") or ""),
305
+ "meta": raw.get("meta", {}) if isinstance(raw.get("meta"), dict) else {},
306
+ }
307
+
308
+
309
+ def _scope(value: dict[str, Any]) -> dict[str, Any]:
310
+ return {
311
+ "scopeId": str(value.get("scopeId", "") or ""),
312
+ "actions": _cleanList(value.get("actions", [])),
313
+ "resources": _cleanList(value.get("resources", [])),
314
+ "expiresAt": str(value.get("expiresAt", "") or ""),
315
+ "tokenRef": str(value.get("tokenRef", "") or ""),
316
+ }
317
+
318
+
319
+ def _credentialRefs(value: Any) -> list[dict[str, str]]:
320
+ refs: list[dict[str, str]] = []
321
+ if not isinstance(value, list):
322
+ return refs
323
+ for item in value:
324
+ if not isinstance(item, dict):
325
+ continue
326
+ ref = str(item.get("ref", item.get("tokenRef", item.get("secretRef", item.get("env", "")))) or "")
327
+ if not ref:
328
+ continue
329
+ refs.append(
330
+ {
331
+ "ref": ref,
332
+ "kind": str(item.get("kind", item.get("type", "secretRef")) or "secretRef"),
333
+ "resource": str(item.get("resource", "") or ""),
334
+ "expiresAt": str(item.get("expiresAt", "") or ""),
335
+ }
336
+ )
337
+ return refs
338
+
339
+
340
+ def _callback(value: Any) -> dict[str, str]:
341
+ raw = value if isinstance(value, dict) else {}
342
+ return {
343
+ "url": str(raw.get("url", raw.get("endpoint", "")) or ""),
344
+ "eventType": str(raw.get("eventType", "") or ""),
345
+ "stateRef": str(raw.get("stateRef", "") or ""),
346
+ }
347
+
348
+
349
+ def _hasRawSecret(value: Any) -> bool:
350
+ if not isinstance(value, dict):
351
+ return False
352
+ for key, item in value.items():
353
+ if key in SECRET_KEYS and str(item or "").strip():
354
+ return True
355
+ return False
356
+
357
+
358
+ def _hasScope(record: dict[str, Any], *, action: str, resource: str = "") -> bool:
359
+ for scope in record.get("scopes", []):
360
+ if action not in scope.get("actions", []):
361
+ continue
362
+ resources = scope.get("resources", [])
363
+ if not resource or "*" in resources or resource in resources:
364
+ return True
365
+ return False
366
+
367
+
368
+ def _cleanList(value: Any) -> list[str]:
369
+ if not isinstance(value, list):
370
+ return []
371
+ deduped = []
372
+ for item in value:
373
+ clean = str(item or "").strip()
374
+ if clean and clean not in deduped:
375
+ deduped.append(clean)
376
+ return deduped
377
+
378
+
379
+ def _kind(value: Any) -> str:
380
+ kind = str(value or "api")
381
+ return kind if kind in CONNECTOR_KINDS else kind
382
+
383
+
384
+ def _status(value: Any) -> str:
385
+ status = str(value or "active")
386
+ return status if status in CONNECTOR_STATUSES else "active"
387
+
388
+
389
+ def _connectionSessionStatus(value: Any) -> str:
390
+ status = str(value or "active")
391
+ return status if status in CONNECTION_SESSION_STATUSES else "active"
392
+
393
+
394
+ def _connectionSessionMode(value: Any) -> str:
395
+ mode = str(value or "clientHostedAdapter")
396
+ return mode if mode in CONNECTION_SESSION_MODES else "clientHostedAdapter"
397
+
398
+
399
+ def _sessionId(payload: dict[str, Any], now: int) -> str:
400
+ base = {k: v for k, v in payload.items() if k != "sessionId"}
401
+ base["createdAt"] = now
402
+ digest = hashlib.sha256(json.dumps(base, ensure_ascii=True, sort_keys=True, default=str).encode("utf-8")).hexdigest()
403
+ return "bridgeSess_" + digest[:24]
404
+
405
+
406
+ def _limit(value: Any) -> int:
407
+ try:
408
+ parsed = int(value)
409
+ except (TypeError, ValueError):
410
+ return 100
411
+ return max(1, min(parsed, 500))
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import time
5
+ from typing import Any
6
+
7
+
8
+ ENVELOPE_STATUSES = {"queued", "delivered", "acknowledged", "failed", "canceled"}
9
+
10
+
11
+ class EnvelopeBook:
12
+ """A2A envelope storage with optional durable backing."""
13
+
14
+ def __init__(self, *, backing: Any | None = None) -> None:
15
+ self.backing = backing
16
+ self._records: dict[str, dict[str, Any]] = {}
17
+
18
+ def saveEnvelope(self, envelope: dict[str, Any], *, status: str = "queued") -> dict[str, Any]:
19
+ msgId = str(envelope.get("msgId", "") or "")
20
+ if not msgId:
21
+ return {"ok": False, "error": "missing:msgId"}
22
+ current = self.getEnvelope(msgId)
23
+ now = int(time.time())
24
+ createdAt = current.get("record", {}).get("createdAt", now) if current.get("ok") else now
25
+ record = _recordFromEnvelope(envelope, status=status, createdAt=createdAt, updatedAt=now)
26
+ self._saveRecord(record)
27
+ return {"ok": True, "record": copy.deepcopy(record)}
28
+
29
+ def getEnvelope(self, msgId: str) -> dict[str, Any]:
30
+ msgId = str(msgId or "")
31
+ record = self._records.get(msgId)
32
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchEnvelope"):
33
+ record = self.backing.fetchEnvelope(msgId)
34
+ if record:
35
+ self._records[msgId] = copy.deepcopy(record)
36
+ if not record:
37
+ return {"ok": False, "error": "envelopeNotFound", "msgId": msgId}
38
+ return {"ok": True, "record": copy.deepcopy(record)}
39
+
40
+ def listEnvelopes(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
41
+ self._loadBacking()
42
+ filters = filters or {}
43
+ status = str(filters.get("status", "") or "")
44
+ fromAgent = str(filters.get("fromAgent", "") or "")
45
+ toAgent = str(filters.get("toAgent", "") or "")
46
+ traceId = str(filters.get("traceId", "") or "")
47
+ taskId = str(filters.get("taskId", "") or "")
48
+ records = list(self._records.values())
49
+ if status:
50
+ records = [record for record in records if record.get("status") == status]
51
+ if fromAgent:
52
+ records = [record for record in records if record.get("fromAgent") == fromAgent]
53
+ if toAgent:
54
+ records = [record for record in records if record.get("toAgent") == toAgent]
55
+ if traceId:
56
+ records = [record for record in records if record.get("traceId") == traceId]
57
+ if taskId:
58
+ records = [record for record in records if record.get("taskId") == taskId]
59
+ records.sort(
60
+ key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("msgId", ""))),
61
+ reverse=True,
62
+ )
63
+ limit = _limit(filters.get("limit", 100))
64
+ return {
65
+ "ok": True,
66
+ "envelopes": [copy.deepcopy(record) for record in records[:limit]],
67
+ "count": min(len(records), limit),
68
+ }
69
+
70
+ def updateEnvelope(self, msgId: str, updates: dict[str, Any]) -> dict[str, Any]:
71
+ current = self.getEnvelope(msgId)
72
+ if not current.get("ok"):
73
+ return current
74
+ record = current["record"]
75
+ delivery = record.get("delivery", {}) if isinstance(record.get("delivery"), dict) else {}
76
+ if isinstance(updates.get("delivery"), dict):
77
+ delivery = copy.deepcopy(updates["delivery"])
78
+ merged = {
79
+ **record,
80
+ "status": _status(updates.get("status", record.get("status", "queued"))),
81
+ "delivery": delivery,
82
+ "errorMsg": str(updates.get("errorMsg", record.get("errorMsg", "")) or ""),
83
+ "updatedAt": int(updates.get("updatedAt", int(time.time())) or int(time.time())),
84
+ }
85
+ self._saveRecord(merged)
86
+ return {"ok": True, "record": copy.deepcopy(merged)}
87
+
88
+ def _saveRecord(self, record: dict[str, Any]) -> None:
89
+ msgId = str(record.get("msgId", "") or "")
90
+ if not msgId:
91
+ return
92
+ self._records[msgId] = copy.deepcopy(record)
93
+ if self._backingEnabled() and hasattr(self.backing, "upsertEnvelope"):
94
+ self.backing.upsertEnvelope(record)
95
+
96
+ def _loadBacking(self) -> None:
97
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listEnvelopes"):
98
+ return
99
+ for record in self.backing.listEnvelopes():
100
+ msgId = str(record.get("msgId", "") or "")
101
+ if msgId:
102
+ self._records[msgId] = copy.deepcopy(record)
103
+
104
+ def _backingEnabled(self) -> bool:
105
+ if self.backing is None:
106
+ return False
107
+ return bool(getattr(self.backing, "enabled", True))
108
+
109
+
110
+ def _recordFromEnvelope(envelope: dict[str, Any], *, status: str, createdAt: int, updatedAt: int) -> dict[str, Any]:
111
+ task = envelope.get("task", {}) if isinstance(envelope.get("task"), dict) else {}
112
+ return {
113
+ "msgId": str(envelope.get("msgId", "") or ""),
114
+ "status": _status(status),
115
+ "fromAgent": str(envelope.get("fromAgent", "") or ""),
116
+ "toAgent": str(envelope.get("toAgent", "") or ""),
117
+ "traceId": str(envelope.get("traceId", "") or ""),
118
+ "taskId": str(task.get("taskId", "") or ""),
119
+ "envelope": copy.deepcopy(envelope),
120
+ "delivery": {},
121
+ "errorMsg": "",
122
+ "createdAt": int(createdAt or updatedAt),
123
+ "updatedAt": int(updatedAt or createdAt),
124
+ }
125
+
126
+
127
+ def _status(value: Any) -> str:
128
+ status = str(value or "queued")
129
+ return status if status in ENVELOPE_STATUSES else "queued"
130
+
131
+
132
+ def _limit(value: Any) -> int:
133
+ try:
134
+ parsed = int(value)
135
+ except (TypeError, ValueError):
136
+ return 100
137
+ return max(1, min(parsed, 500))