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.
- apps/__init__.py +2 -0
- apps/api/__init__.py +2 -0
- apps/api/main.py +652 -0
- apps/benchmarks/__init__.py +1 -0
- apps/benchmarks/main.py +20 -0
- apps/sandbox/__init__.py +1 -0
- apps/sandbox/main.py +20 -0
- apps/worker/__init__.py +2 -0
- apps/worker/main.py +15 -0
- apps/worker/verify.py +14 -0
- patchr/__init__.py +12 -0
- patchr/sdk/__init__.py +20 -0
- patchr/sdk/client.py +12 -0
- patchr-0.1.0.dist-info/METADATA +137 -0
- patchr-0.1.0.dist-info/RECORD +116 -0
- patchr-0.1.0.dist-info/WHEEL +5 -0
- patchr-0.1.0.dist-info/entry_points.txt +5 -0
- patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
- patchr-0.1.0.dist-info/top_level.txt +3 -0
- picux/__init__.py +6 -0
- picux/agents/__init__.py +5 -0
- picux/agents/registry.py +204 -0
- picux/api/__init__.py +5 -0
- picux/api/service.py +5075 -0
- picux/audit/__init__.py +31 -0
- picux/audit/activity.py +97 -0
- picux/audit/observability.py +55 -0
- picux/audit/verification/__init__.py +21 -0
- picux/audit/verification/ledger.py +633 -0
- picux/benchmarks/__init__.py +5 -0
- picux/benchmarks/local.py +286 -0
- picux/config.py +140 -0
- picux/contracts/__init__.py +22 -0
- picux/contracts/handshake.py +122 -0
- picux/contracts/integration.py +385 -0
- picux/contracts/openapi.py +187 -0
- picux/contracts/protocol_map.py +152 -0
- picux/contracts/routes.py +980 -0
- picux/contracts/schema_catalog.py +125 -0
- picux/core/__init__.py +17 -0
- picux/core/models.py +148 -0
- picux/core/router.py +131 -0
- picux/core/runtime.py +42 -0
- picux/core/state_machine.py +38 -0
- picux/domains/__init__.py +2 -0
- picux/domains/bridge/HostRun.py +1104 -0
- picux/domains/bridge/__init__.py +6 -0
- picux/domains/bridge/engine.py +345 -0
- picux/domains/hunt/__init__.py +6 -0
- picux/domains/hunt/engine.py +307 -0
- picux/domains/hunt/models.py +88 -0
- picux/domains/pay/__init__.py +16 -0
- picux/domains/pay/adapters.py +607 -0
- picux/domains/pay/engine.py +950 -0
- picux/domains/pay/models.py +95 -0
- picux/domains/proxy/__init__.py +5 -0
- picux/domains/proxy/engine.py +466 -0
- picux/domains/resolve/__init__.py +5 -0
- picux/domains/resolve/engine.py +546 -0
- picux/orchestrator/__init__.py +3 -0
- picux/orchestrator/engine.py +2840 -0
- picux/portals/__init__.py +17 -0
- picux/portals/templates.py +272 -0
- picux/protocols/__init__.py +1 -0
- picux/protocols/a2a/__init__.py +6 -0
- picux/protocols/a2a/client.py +51 -0
- picux/protocols/a2a/envelope.py +132 -0
- picux/protocols/mcp/__init__.py +7 -0
- picux/protocols/mcp/client.py +69 -0
- picux/protocols/mcp/contract.py +67 -0
- picux/protocols/mcp/server.py +76 -0
- picux/sandbox/__init__.py +6 -0
- picux/sandbox/midnight_arbitrage.py +215 -0
- picux/sandbox/models.py +90 -0
- picux/sdk/__init__.py +13 -0
- picux/sdk/client.py +768 -0
- picux/sdk/external.py +245 -0
- picux/security/__init__.py +18 -0
- picux/security/auth.py +86 -0
- picux/security/config_validator.py +58 -0
- picux/security/policy.py +158 -0
- picux/security/secrets.py +144 -0
- picux/signals/__init__.py +1 -0
- picux/signals/community/__init__.py +24 -0
- picux/signals/community/adapters/__init__.py +7 -0
- picux/signals/community/adapters/reddit.py +37 -0
- picux/signals/community/adapters/shopify.py +23 -0
- picux/signals/community/adapters/web.py +23 -0
- picux/signals/community/disambiguation.py +51 -0
- picux/signals/community/intake.py +227 -0
- picux/signals/community/models.py +102 -0
- picux/signals/community/rules.py +91 -0
- picux/signals/community/scoring.py +64 -0
- picux/storage/__init__.py +41 -0
- picux/storage/agents.py +50 -0
- picux/storage/cases.py +440 -0
- picux/storage/channels.py +476 -0
- picux/storage/connectors.py +411 -0
- picux/storage/envelopes.py +137 -0
- picux/storage/escrows.py +168 -0
- picux/storage/events.py +989 -0
- picux/storage/keyspace.py +60 -0
- picux/storage/mandates.py +107 -0
- picux/storage/portals.py +222 -0
- picux/storage/postgres.py +2049 -0
- picux/storage/providers.py +148 -0
- picux/storage/proxy.py +231 -0
- picux/storage/receipts.py +131 -0
- picux/storage/signals.py +147 -0
- picux/storage/tasks.py +179 -0
- picux/tools/__init__.py +11 -0
- picux/tools/shared.py +2048 -0
- picux/verification/__init__.py +5 -0
- picux/verification/rollout.py +183 -0
- picux/workflows/__init__.py +5 -0
- 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))
|