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,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
LegacyKind = Literal["user", "task", "receipt", "metric"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Keyspace:
|
|
12
|
+
prefix: str = "picux:v1"
|
|
13
|
+
|
|
14
|
+
def user(self, userId: str) -> str:
|
|
15
|
+
return f"{self.prefix}:user:{userId}"
|
|
16
|
+
|
|
17
|
+
def task(self, taskId: str) -> str:
|
|
18
|
+
return f"{self.prefix}:task:{taskId}"
|
|
19
|
+
|
|
20
|
+
def taskLog(self, taskId: str) -> str:
|
|
21
|
+
return f"{self.prefix}:audit:task:{taskId}"
|
|
22
|
+
|
|
23
|
+
def userTasks(self, userId: str) -> str:
|
|
24
|
+
return f"{self.prefix}:userTasks:{userId}"
|
|
25
|
+
|
|
26
|
+
def userLog(self, userId: str) -> str:
|
|
27
|
+
return f"{self.prefix}:audit:user:{userId}"
|
|
28
|
+
|
|
29
|
+
def metric(self, name: str) -> str:
|
|
30
|
+
return f"{self.prefix}:metrics:{name}"
|
|
31
|
+
|
|
32
|
+
def receipt(self, receiptId: str) -> str:
|
|
33
|
+
return f"{self.prefix}:receipt:{receiptId}"
|
|
34
|
+
|
|
35
|
+
def legacyCandidates(self, kind: LegacyKind, itemId: str) -> list[str]:
|
|
36
|
+
item = str(itemId or "")
|
|
37
|
+
if kind == "user":
|
|
38
|
+
return [f"swarm:user:{item}", f"swarm:v2:user:{item}"]
|
|
39
|
+
if kind == "task":
|
|
40
|
+
return [f"swarm:task:{item}", f"swarm:v2:task:{item}"]
|
|
41
|
+
if kind == "receipt":
|
|
42
|
+
return [f"swarm:receipt:{item}", f"swarm:v2:receipt:{item}"]
|
|
43
|
+
if kind == "metric":
|
|
44
|
+
return [f"swarm:metrics:{item}", f"swarm:v2:metrics:{item}"]
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
def migrationReadPlan(self, kind: LegacyKind, itemId: str) -> dict[str, object]:
|
|
48
|
+
current = {
|
|
49
|
+
"user": self.user,
|
|
50
|
+
"task": self.task,
|
|
51
|
+
"receipt": self.receipt,
|
|
52
|
+
"metric": self.metric,
|
|
53
|
+
}[kind](itemId)
|
|
54
|
+
return {
|
|
55
|
+
"kind": kind,
|
|
56
|
+
"id": itemId,
|
|
57
|
+
"writeKey": current,
|
|
58
|
+
"readKeys": [current, *self.legacyCandidates(kind, itemId)],
|
|
59
|
+
"writePolicy": "picuxOnly",
|
|
60
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MandateBook:
|
|
9
|
+
"""Mandate record storage with optional durable backing."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
12
|
+
self.backing = backing
|
|
13
|
+
self._records: dict[str, dict[str, Any]] = {}
|
|
14
|
+
|
|
15
|
+
def saveMandate(self, mandate: dict[str, Any], *, status: str = "pendingVerification") -> dict[str, Any]:
|
|
16
|
+
mandateId = str(mandate.get("mandateId", "") or "")
|
|
17
|
+
if not mandateId:
|
|
18
|
+
return {"ok": False, "error": "missing:mandateId"}
|
|
19
|
+
now = int(time.time())
|
|
20
|
+
current = self.getMandate(mandateId)
|
|
21
|
+
createdAt = current.get("record", {}).get("createdAt", now) if current.get("ok") else now
|
|
22
|
+
record = {
|
|
23
|
+
"mandateId": mandateId,
|
|
24
|
+
"status": status,
|
|
25
|
+
"mandate": copy.deepcopy(mandate),
|
|
26
|
+
"validation": current.get("record", {}).get("validation", {}) if current.get("ok") else {},
|
|
27
|
+
"createdAt": createdAt,
|
|
28
|
+
"updatedAt": now,
|
|
29
|
+
}
|
|
30
|
+
self._saveRecord(record)
|
|
31
|
+
return {"ok": True, "record": copy.deepcopy(record)}
|
|
32
|
+
|
|
33
|
+
def saveValidation(self, mandateId: str, mandate: dict[str, Any], validation: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
status = "verified" if validation.get("ok") else "rejected"
|
|
35
|
+
base = self.saveMandate({**mandate, "mandateId": mandateId}, status=status)
|
|
36
|
+
if not base.get("ok"):
|
|
37
|
+
return base
|
|
38
|
+
record = base["record"]
|
|
39
|
+
record["validation"] = copy.deepcopy(validation)
|
|
40
|
+
record["updatedAt"] = int(time.time())
|
|
41
|
+
self._saveRecord(record)
|
|
42
|
+
return {"ok": True, "record": copy.deepcopy(record)}
|
|
43
|
+
|
|
44
|
+
def getMandate(self, mandateId: str) -> dict[str, Any]:
|
|
45
|
+
mandateId = str(mandateId or "")
|
|
46
|
+
record = self._records.get(mandateId)
|
|
47
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchMandate"):
|
|
48
|
+
record = self.backing.fetchMandate(mandateId)
|
|
49
|
+
if record:
|
|
50
|
+
self._records[mandateId] = copy.deepcopy(record)
|
|
51
|
+
if not record:
|
|
52
|
+
return {"ok": False, "error": "mandateNotFound", "mandateId": mandateId}
|
|
53
|
+
return {"ok": True, "record": copy.deepcopy(record)}
|
|
54
|
+
|
|
55
|
+
def listMandates(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
56
|
+
self._loadBacking()
|
|
57
|
+
filters = filters or {}
|
|
58
|
+
status = str(filters.get("status", "") or "")
|
|
59
|
+
issuerId = str(filters.get("issuerId", "") or "")
|
|
60
|
+
records = list(self._records.values())
|
|
61
|
+
if status:
|
|
62
|
+
records = [record for record in records if record.get("status") == status]
|
|
63
|
+
if issuerId:
|
|
64
|
+
records = [
|
|
65
|
+
record
|
|
66
|
+
for record in records
|
|
67
|
+
if str(record.get("mandate", {}).get("issuer", {}).get("entityId", "") or "") == issuerId
|
|
68
|
+
]
|
|
69
|
+
records.sort(
|
|
70
|
+
key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("mandateId", ""))),
|
|
71
|
+
reverse=True,
|
|
72
|
+
)
|
|
73
|
+
limit = _limit(filters.get("limit", 100))
|
|
74
|
+
return {
|
|
75
|
+
"ok": True,
|
|
76
|
+
"mandates": [copy.deepcopy(record) for record in records[:limit]],
|
|
77
|
+
"count": min(len(records), limit),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def _saveRecord(self, record: dict[str, Any]) -> None:
|
|
81
|
+
mandateId = str(record.get("mandateId", "") or "")
|
|
82
|
+
if not mandateId:
|
|
83
|
+
return
|
|
84
|
+
self._records[mandateId] = copy.deepcopy(record)
|
|
85
|
+
if self._backingEnabled() and hasattr(self.backing, "upsertMandate"):
|
|
86
|
+
self.backing.upsertMandate(record)
|
|
87
|
+
|
|
88
|
+
def _loadBacking(self) -> None:
|
|
89
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listMandates"):
|
|
90
|
+
return
|
|
91
|
+
for record in self.backing.listMandates():
|
|
92
|
+
mandateId = str(record.get("mandateId", "") or "")
|
|
93
|
+
if mandateId:
|
|
94
|
+
self._records[mandateId] = copy.deepcopy(record)
|
|
95
|
+
|
|
96
|
+
def _backingEnabled(self) -> bool:
|
|
97
|
+
if self.backing is None:
|
|
98
|
+
return False
|
|
99
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _limit(value: Any) -> int:
|
|
103
|
+
try:
|
|
104
|
+
parsed = int(value)
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
return 100
|
|
107
|
+
return max(1, min(parsed, 500))
|
picux/storage/portals.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
PORTAL_SESSION_STATUSES = {"active", "needsAuth", "expired", "revoked"}
|
|
11
|
+
PORTAL_ACTION_STATUSES = {"queued", "running", "succeeded", "failed", "blocked", "needsInput", "needsProxy"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PortalBook:
|
|
15
|
+
"""Authenticated portal action state for merchant, carrier, utility, and provider workflows."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
18
|
+
self.backing = backing
|
|
19
|
+
self._sessions: dict[str, dict[str, Any]] = {}
|
|
20
|
+
self._actions: dict[str, dict[str, Any]] = {}
|
|
21
|
+
|
|
22
|
+
def createSession(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
23
|
+
now = int(time.time())
|
|
24
|
+
sessionId = str(payload.get("portalSessionId", payload.get("sessionId", "")) or _stableId("portalSession", payload))
|
|
25
|
+
current = self.getSession(sessionId)
|
|
26
|
+
createdAt = int(current.get("session", {}).get("createdAt", now) if current.get("ok") else now)
|
|
27
|
+
record = _sessionFromPayload({**payload, "portalSessionId": sessionId}, createdAt=createdAt, updatedAt=now)
|
|
28
|
+
self._saveSession(record)
|
|
29
|
+
return {"ok": True, "session": copy.deepcopy(record)}
|
|
30
|
+
|
|
31
|
+
def getSession(self, sessionId: str) -> dict[str, Any]:
|
|
32
|
+
sessionId = str(sessionId or "")
|
|
33
|
+
record = self._sessions.get(sessionId)
|
|
34
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchPortalSession"):
|
|
35
|
+
record = self.backing.fetchPortalSession(sessionId)
|
|
36
|
+
if record:
|
|
37
|
+
self._sessions[sessionId] = copy.deepcopy(record)
|
|
38
|
+
if not record:
|
|
39
|
+
return {"ok": False, "error": "portalSessionNotFound", "portalSessionId": sessionId}
|
|
40
|
+
return {"ok": True, "session": copy.deepcopy(record)}
|
|
41
|
+
|
|
42
|
+
def listSessions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
43
|
+
self._loadSessions()
|
|
44
|
+
filters = filters or {}
|
|
45
|
+
records = list(self._sessions.values())
|
|
46
|
+
for key in ("caseId", "conversationId", "connectorId", "provider", "status"):
|
|
47
|
+
value = str(filters.get(key, "") or "")
|
|
48
|
+
if value:
|
|
49
|
+
records = [record for record in records if str(record.get(key, "") or "") == value]
|
|
50
|
+
records.sort(key=lambda item: (int(item.get("updatedAt", 0) or 0), str(item.get("portalSessionId", ""))), reverse=True)
|
|
51
|
+
limit = _limit(filters.get("limit", 100))
|
|
52
|
+
return {"ok": True, "sessions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
|
|
53
|
+
|
|
54
|
+
def createAction(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
now = int(time.time())
|
|
56
|
+
actionId = str(payload.get("portalActionId", payload.get("actionId", "")) or _stableId("portalAction", payload))
|
|
57
|
+
record = _actionFromPayload({**payload, "portalActionId": actionId}, createdAt=now, updatedAt=now)
|
|
58
|
+
self._saveAction(record)
|
|
59
|
+
return {"ok": True, "action": copy.deepcopy(record)}
|
|
60
|
+
|
|
61
|
+
def getAction(self, actionId: str) -> dict[str, Any]:
|
|
62
|
+
actionId = str(actionId or "")
|
|
63
|
+
record = self._actions.get(actionId)
|
|
64
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchPortalAction"):
|
|
65
|
+
record = self.backing.fetchPortalAction(actionId)
|
|
66
|
+
if record:
|
|
67
|
+
self._actions[actionId] = copy.deepcopy(record)
|
|
68
|
+
if not record:
|
|
69
|
+
return {"ok": False, "error": "portalActionNotFound", "portalActionId": actionId}
|
|
70
|
+
return {"ok": True, "action": copy.deepcopy(record)}
|
|
71
|
+
|
|
72
|
+
def listActions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
73
|
+
self._loadActions()
|
|
74
|
+
filters = filters or {}
|
|
75
|
+
records = list(self._actions.values())
|
|
76
|
+
for key in ("caseId", "conversationId", "portalSessionId", "connectorId", "provider", "status", "kind"):
|
|
77
|
+
value = str(filters.get(key, "") or "")
|
|
78
|
+
if value:
|
|
79
|
+
records = [record for record in records if str(record.get(key, "") or "") == value]
|
|
80
|
+
records.sort(key=lambda item: (int(item.get("updatedAt", 0) or 0), str(item.get("portalActionId", ""))), reverse=True)
|
|
81
|
+
limit = _limit(filters.get("limit", 100))
|
|
82
|
+
return {"ok": True, "actions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
|
|
83
|
+
|
|
84
|
+
def updateAction(self, actionId: str, updates: dict[str, Any]) -> dict[str, Any]:
|
|
85
|
+
current = self.getAction(actionId)
|
|
86
|
+
if not current.get("ok"):
|
|
87
|
+
return current
|
|
88
|
+
record = _actionFromPayload(
|
|
89
|
+
{**current["action"], **updates, "portalActionId": actionId},
|
|
90
|
+
createdAt=int(current["action"].get("createdAt", int(time.time())) or int(time.time())),
|
|
91
|
+
updatedAt=int(time.time()),
|
|
92
|
+
)
|
|
93
|
+
self._saveAction(record)
|
|
94
|
+
return {"ok": True, "action": copy.deepcopy(record)}
|
|
95
|
+
|
|
96
|
+
def appendProof(self, actionId: str, proof: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
current = self.getAction(actionId)
|
|
98
|
+
if not current.get("ok"):
|
|
99
|
+
return current
|
|
100
|
+
action = current["action"]
|
|
101
|
+
proofs = action.get("proofs", []) if isinstance(action.get("proofs"), list) else []
|
|
102
|
+
updated = self.updateAction(actionId, {"proofs": [*proofs, _proof(proof)]})
|
|
103
|
+
if updated.get("ok"):
|
|
104
|
+
updated["proof"] = updated["action"].get("proofs", [])[-1]
|
|
105
|
+
return updated
|
|
106
|
+
|
|
107
|
+
def _saveSession(self, record: dict[str, Any]) -> None:
|
|
108
|
+
sessionId = str(record.get("portalSessionId", "") or "")
|
|
109
|
+
if not sessionId:
|
|
110
|
+
return
|
|
111
|
+
self._sessions[sessionId] = copy.deepcopy(record)
|
|
112
|
+
if self._backingEnabled() and hasattr(self.backing, "upsertPortalSession"):
|
|
113
|
+
self.backing.upsertPortalSession(record)
|
|
114
|
+
|
|
115
|
+
def _saveAction(self, record: dict[str, Any]) -> None:
|
|
116
|
+
actionId = str(record.get("portalActionId", "") or "")
|
|
117
|
+
if not actionId:
|
|
118
|
+
return
|
|
119
|
+
self._actions[actionId] = copy.deepcopy(record)
|
|
120
|
+
if self._backingEnabled() and hasattr(self.backing, "upsertPortalAction"):
|
|
121
|
+
self.backing.upsertPortalAction(record)
|
|
122
|
+
|
|
123
|
+
def _loadSessions(self) -> None:
|
|
124
|
+
if self._backingEnabled() and hasattr(self.backing, "listPortalSessions"):
|
|
125
|
+
for record in self.backing.listPortalSessions():
|
|
126
|
+
if record.get("portalSessionId"):
|
|
127
|
+
self._sessions[str(record["portalSessionId"])] = copy.deepcopy(record)
|
|
128
|
+
|
|
129
|
+
def _loadActions(self) -> None:
|
|
130
|
+
if self._backingEnabled() and hasattr(self.backing, "listPortalActions"):
|
|
131
|
+
for record in self.backing.listPortalActions():
|
|
132
|
+
if record.get("portalActionId"):
|
|
133
|
+
self._actions[str(record["portalActionId"])] = copy.deepcopy(record)
|
|
134
|
+
|
|
135
|
+
def _backingEnabled(self) -> bool:
|
|
136
|
+
return bool(self.backing is not None and getattr(self.backing, "enabled", True))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _sessionFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
|
|
140
|
+
return {
|
|
141
|
+
"portalSessionId": str(payload.get("portalSessionId", payload.get("sessionId", "")) or ""),
|
|
142
|
+
"caseId": str(payload.get("caseId", "") or ""),
|
|
143
|
+
"conversationId": str(payload.get("conversationId", "") or ""),
|
|
144
|
+
"connectorId": str(payload.get("connectorId", "") or ""),
|
|
145
|
+
"provider": str(payload.get("provider", "") or ""),
|
|
146
|
+
"baseUrl": str(payload.get("baseUrl", payload.get("url", "")) or ""),
|
|
147
|
+
"status": _choice(payload.get("status", "active"), PORTAL_SESSION_STATUSES, "active"),
|
|
148
|
+
"credentialRef": str(payload.get("credentialRef", "") or ""),
|
|
149
|
+
"storageStateRef": str(payload.get("storageStateRef", "") or ""),
|
|
150
|
+
"mfa": payload.get("mfa", {}) if isinstance(payload.get("mfa"), dict) else {},
|
|
151
|
+
"meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
|
|
152
|
+
"createdAt": int(createdAt),
|
|
153
|
+
"updatedAt": int(updatedAt),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _actionFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
|
|
158
|
+
return {
|
|
159
|
+
"portalActionId": str(payload.get("portalActionId", payload.get("actionId", "")) or ""),
|
|
160
|
+
"portalSessionId": str(payload.get("portalSessionId", payload.get("sessionId", "")) or ""),
|
|
161
|
+
"templateId": str(payload.get("templateId", "") or ""),
|
|
162
|
+
"caseId": str(payload.get("caseId", "") or ""),
|
|
163
|
+
"conversationId": str(payload.get("conversationId", "") or ""),
|
|
164
|
+
"connectorId": str(payload.get("connectorId", "") or ""),
|
|
165
|
+
"provider": str(payload.get("provider", "") or ""),
|
|
166
|
+
"kind": str(payload.get("kind", payload.get("action", "submitForm")) or "submitForm"),
|
|
167
|
+
"targetUrl": str(payload.get("targetUrl", payload.get("url", "")) or ""),
|
|
168
|
+
"status": _choice(payload.get("status", "queued"), PORTAL_ACTION_STATUSES, "queued"),
|
|
169
|
+
"steps": [_step(item) for item in payload.get("steps", []) if isinstance(item, dict)],
|
|
170
|
+
"inputs": payload.get("inputs", {}) if isinstance(payload.get("inputs"), dict) else {},
|
|
171
|
+
"proofs": [_proof(item) for item in payload.get("proofs", []) if isinstance(item, dict)],
|
|
172
|
+
"result": payload.get("result", {}) if isinstance(payload.get("result"), dict) else {},
|
|
173
|
+
"error": str(payload.get("error", "") or ""),
|
|
174
|
+
"requiresProxy": bool(payload.get("requiresProxy", False)),
|
|
175
|
+
"meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
|
|
176
|
+
"createdAt": int(createdAt),
|
|
177
|
+
"updatedAt": int(updatedAt),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _step(payload: dict[str, Any]) -> dict[str, Any]:
|
|
182
|
+
return {
|
|
183
|
+
"stepId": str(payload.get("stepId", "") or _stableId("portalStep", payload)),
|
|
184
|
+
"label": str(payload.get("label", payload.get("action", "step")) or "step"),
|
|
185
|
+
"action": str(payload.get("action", "") or ""),
|
|
186
|
+
"selector": str(payload.get("selector", "") or ""),
|
|
187
|
+
"valueKey": str(payload.get("valueKey", "") or ""),
|
|
188
|
+
"status": str(payload.get("status", "pending") or "pending"),
|
|
189
|
+
"proofRequired": bool(payload.get("proofRequired", False)),
|
|
190
|
+
"meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _proof(payload: dict[str, Any]) -> dict[str, Any]:
|
|
195
|
+
return {
|
|
196
|
+
"proofId": str(payload.get("proofId", "") or _stableId("portalProof", payload)),
|
|
197
|
+
"kind": str(payload.get("kind", "screenshot") or "screenshot"),
|
|
198
|
+
"status": str(payload.get("status", "captured") or "captured"),
|
|
199
|
+
"label": str(payload.get("label", "") or ""),
|
|
200
|
+
"artifactRef": str(payload.get("artifactRef", payload.get("artifactId", "")) or ""),
|
|
201
|
+
"sourceUrl": str(payload.get("sourceUrl", "") or ""),
|
|
202
|
+
"hash": str(payload.get("hash", payload.get("sha256", "")) or ""),
|
|
203
|
+
"createdAt": int(payload.get("createdAt", int(time.time())) or int(time.time())),
|
|
204
|
+
"meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _choice(value: Any, allowed: set[str], default: str) -> str:
|
|
209
|
+
clean = str(value or default)
|
|
210
|
+
return clean if clean in allowed else default
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _limit(value: Any) -> int:
|
|
214
|
+
try:
|
|
215
|
+
return max(1, min(int(value or 100), 500))
|
|
216
|
+
except Exception:
|
|
217
|
+
return 100
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _stableId(prefix: str, payload: dict[str, Any]) -> str:
|
|
221
|
+
digest = hashlib.sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True).encode("utf-8")).hexdigest()
|
|
222
|
+
return f"{prefix}_{digest[:24]}"
|