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,633 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ALLOWED_RECEIPT_STATUS = {"settled", "rejected", "frozen"}
|
|
12
|
+
Clock = Callable[[], datetime | str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def canonicalHash(payload: dict[str, Any]) -> str:
|
|
16
|
+
"""Return a stable digest for agent-readable audit payloads."""
|
|
17
|
+
|
|
18
|
+
return hashlib.sha256(_canonicalJson(payload).encode("utf-8")).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class EvidenceArtifact:
|
|
23
|
+
artifactId: str
|
|
24
|
+
kind: str
|
|
25
|
+
source: str
|
|
26
|
+
payload: dict[str, Any]
|
|
27
|
+
payloadHash: str
|
|
28
|
+
createdAt: str
|
|
29
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def fromPayload(
|
|
33
|
+
cls,
|
|
34
|
+
payload: dict[str, Any],
|
|
35
|
+
*,
|
|
36
|
+
clock: Clock,
|
|
37
|
+
) -> "EvidenceArtifact":
|
|
38
|
+
kind = str(payload.get("kind", payload.get("type", "evidence")) or "evidence")
|
|
39
|
+
source = str(payload.get("source", payload.get("uri", "inline")) or "inline")
|
|
40
|
+
body = payload.get("payload") if isinstance(payload.get("payload"), dict) else payload.get("data", {})
|
|
41
|
+
if not isinstance(body, dict):
|
|
42
|
+
body = {"value": body}
|
|
43
|
+
meta = payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {}
|
|
44
|
+
createdAt = _time(clock)
|
|
45
|
+
base = {
|
|
46
|
+
"kind": kind,
|
|
47
|
+
"source": source,
|
|
48
|
+
"payload": body,
|
|
49
|
+
"meta": meta,
|
|
50
|
+
}
|
|
51
|
+
return cls(
|
|
52
|
+
artifactId="art_" + canonicalHash(base)[:24],
|
|
53
|
+
kind=kind,
|
|
54
|
+
source=source,
|
|
55
|
+
payload=body,
|
|
56
|
+
payloadHash=canonicalHash(body),
|
|
57
|
+
createdAt=createdAt,
|
|
58
|
+
meta=meta,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def toMap(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"artifactId": self.artifactId,
|
|
64
|
+
"kind": self.kind,
|
|
65
|
+
"source": self.source,
|
|
66
|
+
"payload": self.payload,
|
|
67
|
+
"payloadHash": self.payloadHash,
|
|
68
|
+
"createdAt": self.createdAt,
|
|
69
|
+
"meta": self.meta,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ProofOfValueRecord:
|
|
75
|
+
povId: str
|
|
76
|
+
taskId: str
|
|
77
|
+
domain: str
|
|
78
|
+
rules: tuple[str, ...]
|
|
79
|
+
value: dict[str, Any]
|
|
80
|
+
artifacts: tuple[dict[str, Any], ...]
|
|
81
|
+
prevHash: str
|
|
82
|
+
chainHash: str
|
|
83
|
+
createdAt: str
|
|
84
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def fromPayload(cls, payload: dict[str, Any], *, clock: Clock, prevHash: str = "") -> "ProofOfValueRecord":
|
|
88
|
+
taskId = str(payload.get("taskId", "") or "")
|
|
89
|
+
domain = str(payload.get("domain", "") or "")
|
|
90
|
+
rules = tuple(str(item) for item in payload.get("rules", []) if str(item).strip())
|
|
91
|
+
value = payload.get("value") if isinstance(payload.get("value"), dict) else {}
|
|
92
|
+
meta = payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {}
|
|
93
|
+
artifacts = tuple(_normalizeArtifact(item) for item in payload.get("artifacts", []) if isinstance(item, dict))
|
|
94
|
+
createdAt = _time(clock)
|
|
95
|
+
base = {
|
|
96
|
+
"taskId": taskId,
|
|
97
|
+
"domain": domain,
|
|
98
|
+
"rules": list(rules),
|
|
99
|
+
"value": value,
|
|
100
|
+
"artifacts": list(artifacts),
|
|
101
|
+
"prevHash": prevHash,
|
|
102
|
+
"createdAt": createdAt,
|
|
103
|
+
"meta": meta,
|
|
104
|
+
}
|
|
105
|
+
chainHash = canonicalHash(base)
|
|
106
|
+
return cls(
|
|
107
|
+
povId="pov_" + chainHash[:24],
|
|
108
|
+
taskId=taskId,
|
|
109
|
+
domain=domain,
|
|
110
|
+
rules=rules,
|
|
111
|
+
value=value,
|
|
112
|
+
artifacts=artifacts,
|
|
113
|
+
prevHash=prevHash,
|
|
114
|
+
chainHash=chainHash,
|
|
115
|
+
createdAt=createdAt,
|
|
116
|
+
meta=meta,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def toMap(self) -> dict[str, Any]:
|
|
120
|
+
return {
|
|
121
|
+
"povId": self.povId,
|
|
122
|
+
"taskId": self.taskId,
|
|
123
|
+
"domain": self.domain,
|
|
124
|
+
"rules": list(self.rules),
|
|
125
|
+
"value": self.value,
|
|
126
|
+
"artifacts": list(self.artifacts),
|
|
127
|
+
"prevHash": self.prevHash,
|
|
128
|
+
"chainHash": self.chainHash,
|
|
129
|
+
"createdAt": self.createdAt,
|
|
130
|
+
"meta": self.meta,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class VerificationLedger:
|
|
135
|
+
"""Deterministic audit ledger for evidence, proof-of-value, receipts, and proof packs."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, *, clock: Clock | None = None, backing: Any | None = None) -> None:
|
|
138
|
+
self.clock = clock or (lambda: datetime.now(timezone.utc))
|
|
139
|
+
self.backing = backing
|
|
140
|
+
self._artifacts: dict[str, dict[str, Any]] = {}
|
|
141
|
+
self._pov: dict[str, dict[str, Any]] = {}
|
|
142
|
+
self._proofPacks: dict[str, dict[str, Any]] = {}
|
|
143
|
+
self._chainTail = ""
|
|
144
|
+
|
|
145
|
+
def recordEvidence(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
146
|
+
artifact = EvidenceArtifact.fromPayload(payload, clock=self.clock).toMap()
|
|
147
|
+
self._artifacts[artifact["artifactId"]] = artifact
|
|
148
|
+
if self._backingEnabled() and hasattr(self.backing, "insertEvidence"):
|
|
149
|
+
self.backing.insertEvidence(artifact)
|
|
150
|
+
return {"ok": True, "artifact": artifact}
|
|
151
|
+
|
|
152
|
+
def recordPov(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
errors = _povInputErrors(payload)
|
|
154
|
+
if errors:
|
|
155
|
+
return {"ok": False, "errors": errors}
|
|
156
|
+
normalized = dict(payload)
|
|
157
|
+
normalized["artifacts"] = self._resolveArtifacts(payload.get("artifacts", []))
|
|
158
|
+
prevHash = str(payload.get("prevHash", self._chainTail) or "")
|
|
159
|
+
pov = ProofOfValueRecord.fromPayload(normalized, clock=self.clock, prevHash=prevHash).toMap()
|
|
160
|
+
self._pov[pov["povId"]] = pov
|
|
161
|
+
self._chainTail = pov["chainHash"]
|
|
162
|
+
return {"ok": True, "pov": pov}
|
|
163
|
+
|
|
164
|
+
def verifyReceipt(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
165
|
+
receipt = payload.get("receipt") if isinstance(payload.get("receipt"), dict) else payload
|
|
166
|
+
pov = payload.get("pov") if isinstance(payload.get("pov"), dict) else {}
|
|
167
|
+
if not pov and receipt.get("povRef"):
|
|
168
|
+
pov = self._pov.get(str(receipt.get("povRef")), {})
|
|
169
|
+
|
|
170
|
+
errors = self._receiptErrors(receipt, pov)
|
|
171
|
+
return {
|
|
172
|
+
"ok": not errors,
|
|
173
|
+
"receiptId": str(receipt.get("receiptId", "") or ""),
|
|
174
|
+
"povId": str(pov.get("povId", "") or receipt.get("povRef", "") or ""),
|
|
175
|
+
"errors": errors,
|
|
176
|
+
"hash": receiptHash(receipt),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def verifyChain(self, records: list[dict[str, Any]]) -> dict[str, Any]:
|
|
180
|
+
errors: list[str] = []
|
|
181
|
+
prevHash = ""
|
|
182
|
+
for index, record in enumerate(records):
|
|
183
|
+
expectedPrev = str(record.get("prevHash", "") or "")
|
|
184
|
+
if expectedPrev != prevHash:
|
|
185
|
+
errors.append(f"chainBreak:{index}")
|
|
186
|
+
expected = povChainHash(record)
|
|
187
|
+
if str(record.get("chainHash", "") or "") != expected:
|
|
188
|
+
errors.append(f"hashMismatch:{index}")
|
|
189
|
+
prevHash = str(record.get("chainHash", "") or "")
|
|
190
|
+
return {"ok": not errors, "errors": errors, "count": len(records)}
|
|
191
|
+
|
|
192
|
+
def createProofPack(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
artifacts = self._resolveArtifacts(payload.get("artifacts", []))
|
|
194
|
+
povRecords = self._resolvePov(payload.get("pov", payload.get("povRecords", [])))
|
|
195
|
+
receipts = _cleanDicts(payload.get("receipts", []))
|
|
196
|
+
decisions = _cleanDicts(payload.get("decisions", payload.get("decisionLogs", [])))
|
|
197
|
+
actors = _cleanDicts(payload.get("actors", []))
|
|
198
|
+
snapshots = _cleanDicts(payload.get("sourceSnapshots", payload.get("snapshots", [])))
|
|
199
|
+
typedEvidence = _cleanDicts(payload.get("typedEvidence", payload.get("vaultEvidence", [])))
|
|
200
|
+
fraudIndicators = _cleanDicts(payload.get("fraudIndicators", []))
|
|
201
|
+
sourceClasses = _cleanDicts(payload.get("sourceClasses", []))
|
|
202
|
+
caseOpsTimeline = _cleanDicts(payload.get("caseOpsTimeline", []))
|
|
203
|
+
timeline = _proofTimeline(payload, artifacts, povRecords, receipts, decisions, snapshots)
|
|
204
|
+
share = _proofShare(payload.get("share", {}))
|
|
205
|
+
createdAt = _time(self.clock)
|
|
206
|
+
base = {
|
|
207
|
+
"status": str(payload.get("status", "ready") or "ready"),
|
|
208
|
+
"taskId": str(payload.get("taskId", "") or ""),
|
|
209
|
+
"domain": str(payload.get("domain", "") or ""),
|
|
210
|
+
"title": str(payload.get("title", "Portable case file") or "Portable case file"),
|
|
211
|
+
"summary": str(payload.get("summary", "") or ""),
|
|
212
|
+
"artifacts": artifacts,
|
|
213
|
+
"pov": povRecords,
|
|
214
|
+
"receipts": receipts,
|
|
215
|
+
"decisions": decisions,
|
|
216
|
+
"actors": actors,
|
|
217
|
+
"timeline": timeline,
|
|
218
|
+
"vault": {
|
|
219
|
+
"kind": "autoAuditVault",
|
|
220
|
+
"sourceSnapshots": snapshots,
|
|
221
|
+
"decisionLogs": decisions,
|
|
222
|
+
"complaintEvidence": artifacts,
|
|
223
|
+
"typedEvidence": typedEvidence,
|
|
224
|
+
"fraudIndicators": fraudIndicators,
|
|
225
|
+
"sourceClasses": sourceClasses,
|
|
226
|
+
"pov": povRecords,
|
|
227
|
+
},
|
|
228
|
+
"share": share,
|
|
229
|
+
"meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
|
|
230
|
+
"caseOpsTimeline": caseOpsTimeline,
|
|
231
|
+
"createdAt": createdAt,
|
|
232
|
+
}
|
|
233
|
+
chainHash = canonicalHash(base)
|
|
234
|
+
packId = _proofPackId(payload.get("packId", ""), chainHash)
|
|
235
|
+
proofCard = _proofCard(payload, packId, chainHash, base)
|
|
236
|
+
pack = {"packId": packId, **base, "proofCard": proofCard, "chainHash": chainHash}
|
|
237
|
+
self._proofPacks[packId] = pack
|
|
238
|
+
return {"ok": True, "proofPack": pack}
|
|
239
|
+
|
|
240
|
+
def getPov(self, povId: str) -> dict[str, Any]:
|
|
241
|
+
pov = self._pov.get(povId)
|
|
242
|
+
if not pov:
|
|
243
|
+
return {"ok": False, "error": "povNotFound", "povId": povId}
|
|
244
|
+
return {"ok": True, "pov": pov}
|
|
245
|
+
|
|
246
|
+
def getEvidence(self, artifactId: str) -> dict[str, Any]:
|
|
247
|
+
artifactId = str(artifactId or "")
|
|
248
|
+
artifact = self._artifacts.get(artifactId)
|
|
249
|
+
if artifact is None and self._backingEnabled() and hasattr(self.backing, "fetchEvidence"):
|
|
250
|
+
artifact = self.backing.fetchEvidence(artifactId)
|
|
251
|
+
if artifact:
|
|
252
|
+
self._artifacts[artifactId] = artifact
|
|
253
|
+
if not artifact:
|
|
254
|
+
return {"ok": False, "error": "artifactNotFound", "artifactId": artifactId}
|
|
255
|
+
return {"ok": True, "artifact": artifact}
|
|
256
|
+
|
|
257
|
+
def listEvidence(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
258
|
+
self._loadEvidence()
|
|
259
|
+
filters = filters or {}
|
|
260
|
+
kind = str(filters.get("kind", "") or "")
|
|
261
|
+
source = str(filters.get("source", "") or "")
|
|
262
|
+
payloadHash = str(filters.get("payloadHash", "") or "")
|
|
263
|
+
records = list(self._artifacts.values())
|
|
264
|
+
if kind:
|
|
265
|
+
records = [record for record in records if record.get("kind") == kind]
|
|
266
|
+
if source:
|
|
267
|
+
records = [record for record in records if record.get("source") == source]
|
|
268
|
+
if payloadHash:
|
|
269
|
+
records = [record for record in records if record.get("payloadHash") == payloadHash]
|
|
270
|
+
records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("artifactId", ""))), reverse=True)
|
|
271
|
+
limit = _limit(filters.get("limit", 100))
|
|
272
|
+
return {
|
|
273
|
+
"ok": True,
|
|
274
|
+
"artifacts": [dict(record) for record in records[:limit]],
|
|
275
|
+
"count": min(len(records), limit),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
def getProofPack(self, packId: str) -> dict[str, Any]:
|
|
279
|
+
packId = str(packId or "")
|
|
280
|
+
proofPack = self._proofPacks.get(packId)
|
|
281
|
+
if not proofPack:
|
|
282
|
+
return {"ok": False, "error": "proofPackNotFound", "packId": packId}
|
|
283
|
+
return {"ok": True, "proofPack": dict(proofPack)}
|
|
284
|
+
|
|
285
|
+
def listProofPacks(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
286
|
+
filters = filters or {}
|
|
287
|
+
taskId = str(filters.get("taskId", "") or "")
|
|
288
|
+
domain = str(filters.get("domain", "") or "")
|
|
289
|
+
status = str(filters.get("status", "") or "")
|
|
290
|
+
records = list(self._proofPacks.values())
|
|
291
|
+
if taskId:
|
|
292
|
+
records = [record for record in records if record.get("taskId") == taskId]
|
|
293
|
+
if domain:
|
|
294
|
+
records = [record for record in records if record.get("domain") == domain]
|
|
295
|
+
if status:
|
|
296
|
+
records = [record for record in records if record.get("status") == status]
|
|
297
|
+
records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("packId", ""))), reverse=True)
|
|
298
|
+
limit = _limit(filters.get("limit", 100))
|
|
299
|
+
return {
|
|
300
|
+
"ok": True,
|
|
301
|
+
"proofPacks": [dict(record) for record in records[:limit]],
|
|
302
|
+
"count": min(len(records), limit),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def _loadEvidence(self) -> None:
|
|
306
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listEvidence"):
|
|
307
|
+
return
|
|
308
|
+
for artifact in self.backing.listEvidence():
|
|
309
|
+
artifactId = str(artifact.get("artifactId", "") or "")
|
|
310
|
+
if artifactId:
|
|
311
|
+
self._artifacts[artifactId] = artifact
|
|
312
|
+
|
|
313
|
+
def _backingEnabled(self) -> bool:
|
|
314
|
+
if self.backing is None:
|
|
315
|
+
return False
|
|
316
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
317
|
+
|
|
318
|
+
def _resolveArtifacts(self, artifacts: Any) -> list[dict[str, Any]]:
|
|
319
|
+
resolved: list[dict[str, Any]] = []
|
|
320
|
+
if not isinstance(artifacts, list):
|
|
321
|
+
return resolved
|
|
322
|
+
for item in artifacts:
|
|
323
|
+
if isinstance(item, str) and item in self._artifacts:
|
|
324
|
+
resolved.append(self._artifacts[item])
|
|
325
|
+
elif isinstance(item, dict):
|
|
326
|
+
artifactId = str(item.get("artifactId", "") or "")
|
|
327
|
+
resolved.append(self._artifacts.get(artifactId, _normalizeArtifact(item)))
|
|
328
|
+
return resolved
|
|
329
|
+
|
|
330
|
+
def _resolvePov(self, records: Any) -> list[dict[str, Any]]:
|
|
331
|
+
resolved: list[dict[str, Any]] = []
|
|
332
|
+
if isinstance(records, dict):
|
|
333
|
+
records = [records]
|
|
334
|
+
if isinstance(records, str):
|
|
335
|
+
records = [records]
|
|
336
|
+
if not isinstance(records, list):
|
|
337
|
+
return resolved
|
|
338
|
+
for item in records:
|
|
339
|
+
if isinstance(item, str) and item in self._pov:
|
|
340
|
+
resolved.append(dict(self._pov[item]))
|
|
341
|
+
elif isinstance(item, dict):
|
|
342
|
+
povId = str(item.get("povId", "") or "")
|
|
343
|
+
resolved.append(dict(self._pov.get(povId, item)))
|
|
344
|
+
return resolved
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _receiptErrors(receipt: dict[str, Any], pov: dict[str, Any]) -> list[str]:
|
|
348
|
+
errors: list[str] = []
|
|
349
|
+
if not receipt:
|
|
350
|
+
return ["missing:receipt"]
|
|
351
|
+
if str(receipt.get("receiptId", "") or "") != receiptId(receipt):
|
|
352
|
+
errors.append("receiptHashMismatch")
|
|
353
|
+
if str(receipt.get("status", "") or "") not in ALLOWED_RECEIPT_STATUS:
|
|
354
|
+
errors.append("invalid:status")
|
|
355
|
+
if not str(receipt.get("taskId", "") or ""):
|
|
356
|
+
errors.append("missing:taskId")
|
|
357
|
+
if not str(receipt.get("mandateId", "") or ""):
|
|
358
|
+
errors.append("missing:mandateId")
|
|
359
|
+
|
|
360
|
+
povRef = str(receipt.get("povRef", "") or "")
|
|
361
|
+
povId = str(pov.get("povId", "") or "")
|
|
362
|
+
if povRef and povId and povRef != povId:
|
|
363
|
+
errors.append("povMismatch")
|
|
364
|
+
if pov and str(pov.get("taskId", "") or "") and str(receipt.get("taskId", "") or "") != str(
|
|
365
|
+
pov.get("taskId", "") or ""
|
|
366
|
+
):
|
|
367
|
+
errors.append("taskMismatch")
|
|
368
|
+
value = pov.get("value", {}) if isinstance(pov.get("value"), dict) else {}
|
|
369
|
+
amount = value.get("amount")
|
|
370
|
+
currency = str(value.get("currency", "") or "")
|
|
371
|
+
receiptAmount = _amount(receipt.get("amount", 0.0))
|
|
372
|
+
povAmount = _amount(amount)
|
|
373
|
+
if amount is not None and (receiptAmount is None or povAmount is None):
|
|
374
|
+
errors.append("invalid:amount")
|
|
375
|
+
elif amount is not None and round(receiptAmount or 0.0, 2) != round(povAmount or 0.0, 2):
|
|
376
|
+
errors.append("amountMismatch")
|
|
377
|
+
if currency and str(receipt.get("currency", "") or "").upper() != currency.upper():
|
|
378
|
+
errors.append("currencyMismatch")
|
|
379
|
+
return errors
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def receiptHash(receipt: dict[str, Any]) -> str:
|
|
383
|
+
return canonicalHash(receiptBase(receipt))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def receiptId(receipt: dict[str, Any]) -> str:
|
|
387
|
+
return "rcpt_" + receiptHash(receipt)[:24]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def receiptBase(receipt: dict[str, Any]) -> dict[str, Any]:
|
|
391
|
+
return {
|
|
392
|
+
"mandateId": str(receipt.get("mandateId", "") or ""),
|
|
393
|
+
"taskId": str(receipt.get("taskId", "") or ""),
|
|
394
|
+
"status": str(receipt.get("status", "") or ""),
|
|
395
|
+
"amount": receipt.get("amount", 0.0),
|
|
396
|
+
"currency": str(receipt.get("currency", "") or ""),
|
|
397
|
+
"povRef": str(receipt.get("povRef", "") or ""),
|
|
398
|
+
"errors": receipt.get("errors", []) if isinstance(receipt.get("errors", []), list) else [],
|
|
399
|
+
"createdAt": str(receipt.get("createdAt", "") or ""),
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def povChainHash(record: dict[str, Any]) -> str:
|
|
404
|
+
base = {
|
|
405
|
+
"taskId": str(record.get("taskId", "") or ""),
|
|
406
|
+
"domain": str(record.get("domain", "") or ""),
|
|
407
|
+
"rules": list(record.get("rules", []) if isinstance(record.get("rules"), list) else []),
|
|
408
|
+
"value": record.get("value", {}) if isinstance(record.get("value"), dict) else {},
|
|
409
|
+
"artifacts": list(record.get("artifacts", []) if isinstance(record.get("artifacts"), list) else []),
|
|
410
|
+
"prevHash": str(record.get("prevHash", "") or ""),
|
|
411
|
+
"createdAt": str(record.get("createdAt", "") or ""),
|
|
412
|
+
"meta": record.get("meta", {}) if isinstance(record.get("meta"), dict) else {},
|
|
413
|
+
}
|
|
414
|
+
return canonicalHash(base)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _canonicalJson(payload: dict[str, Any]) -> str:
|
|
418
|
+
return json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _normalizeArtifact(item: dict[str, Any]) -> dict[str, Any]:
|
|
422
|
+
if "artifactId" in item and "payloadHash" in item:
|
|
423
|
+
return item
|
|
424
|
+
payload = item.get("payload") if isinstance(item.get("payload"), dict) else item
|
|
425
|
+
return {
|
|
426
|
+
"artifactId": str(item.get("artifactId", "") or "art_" + canonicalHash(payload)[:24]),
|
|
427
|
+
"kind": str(item.get("kind", "evidence") or "evidence"),
|
|
428
|
+
"source": str(item.get("source", "inline") or "inline"),
|
|
429
|
+
"payload": payload,
|
|
430
|
+
"payloadHash": canonicalHash(payload),
|
|
431
|
+
"createdAt": str(item.get("createdAt", "") or ""),
|
|
432
|
+
"meta": item.get("meta", {}) if isinstance(item.get("meta"), dict) else {},
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _proofTimeline(
|
|
437
|
+
payload: dict[str, Any],
|
|
438
|
+
artifacts: list[dict[str, Any]],
|
|
439
|
+
povRecords: list[dict[str, Any]],
|
|
440
|
+
receipts: list[dict[str, Any]],
|
|
441
|
+
decisions: list[dict[str, Any]],
|
|
442
|
+
snapshots: list[dict[str, Any]],
|
|
443
|
+
) -> list[dict[str, Any]]:
|
|
444
|
+
entries = _cleanDicts(payload.get("timeline", []))
|
|
445
|
+
for artifact in artifacts:
|
|
446
|
+
entries.append(
|
|
447
|
+
{
|
|
448
|
+
"at": str(artifact.get("createdAt", "") or ""),
|
|
449
|
+
"kind": "evidence",
|
|
450
|
+
"ref": str(artifact.get("artifactId", "") or ""),
|
|
451
|
+
"label": str(artifact.get("kind", "evidence") or "evidence"),
|
|
452
|
+
"source": str(artifact.get("source", "") or ""),
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
for pov in povRecords:
|
|
456
|
+
entries.append(
|
|
457
|
+
{
|
|
458
|
+
"at": str(pov.get("createdAt", "") or ""),
|
|
459
|
+
"kind": "pov",
|
|
460
|
+
"ref": str(pov.get("povId", "") or ""),
|
|
461
|
+
"label": str(pov.get("domain", "proof") or "proof"),
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
for receipt in receipts:
|
|
465
|
+
entries.append(
|
|
466
|
+
{
|
|
467
|
+
"at": str(receipt.get("createdAt", "") or ""),
|
|
468
|
+
"kind": "receipt",
|
|
469
|
+
"ref": str(receipt.get("receiptId", "") or ""),
|
|
470
|
+
"label": str(receipt.get("status", "receipt") or "receipt"),
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
for decision in decisions:
|
|
474
|
+
entries.append(
|
|
475
|
+
{
|
|
476
|
+
"at": str(decision.get("at", decision.get("createdAt", "")) or ""),
|
|
477
|
+
"kind": "decision",
|
|
478
|
+
"ref": str(decision.get("decisionId", decision.get("ref", "")) or ""),
|
|
479
|
+
"label": str(decision.get("label", decision.get("reason", "decision")) or "decision"),
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
for snapshot in snapshots:
|
|
483
|
+
entries.append(
|
|
484
|
+
{
|
|
485
|
+
"at": str(snapshot.get("capturedAt", snapshot.get("at", "")) or ""),
|
|
486
|
+
"kind": "sourceSnapshot",
|
|
487
|
+
"ref": str(snapshot.get("snapshotId", snapshot.get("url", "")) or ""),
|
|
488
|
+
"label": str(snapshot.get("label", "source snapshot") or "source snapshot"),
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
return sorted(entries, key=lambda item: (str(item.get("at", "")), str(item.get("kind", "")), str(item.get("ref", ""))))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _proofShare(value: Any) -> dict[str, Any]:
|
|
495
|
+
raw = value if isinstance(value, dict) else {}
|
|
496
|
+
formats = raw.get("formats", ["json"]) if isinstance(raw.get("formats", ["json"]), list) else ["json"]
|
|
497
|
+
share = {
|
|
498
|
+
"type": str(raw.get("type", "portableCaseFile") or "portableCaseFile"),
|
|
499
|
+
"label": str(raw.get("label", "From opaque workflow to portable case file.") or ""),
|
|
500
|
+
"audience": str(raw.get("audience", "external") or "external"),
|
|
501
|
+
"redacted": bool(raw.get("redacted", True)),
|
|
502
|
+
"formats": _cleanStrings(formats),
|
|
503
|
+
}
|
|
504
|
+
if raw.get("url"):
|
|
505
|
+
share["url"] = str(raw.get("url") or "")
|
|
506
|
+
return share
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _proofCard(payload: dict[str, Any], packId: str, chainHash: str, base: dict[str, Any]) -> dict[str, Any]:
|
|
510
|
+
raw = payload.get("proofCard", {}) if isinstance(payload.get("proofCard"), dict) else {}
|
|
511
|
+
openQuestions = _cleanStrings(raw.get("openQuestions", payload.get("openQuestions", [])))
|
|
512
|
+
escalation = raw.get("escalation", payload.get("escalation", {}))
|
|
513
|
+
escalation = escalation if isinstance(escalation, dict) else {}
|
|
514
|
+
if openQuestions and "recommended" not in escalation:
|
|
515
|
+
escalation = {"recommended": True, "routeDomain": "proxy", "reason": "openQuestions"}
|
|
516
|
+
elif not escalation:
|
|
517
|
+
escalation = {"recommended": False, "routeDomain": "", "reason": ""}
|
|
518
|
+
evidenceStatus = str(raw.get("evidenceStatus", payload.get("evidenceStatus", "")) or "")
|
|
519
|
+
if not evidenceStatus:
|
|
520
|
+
evidenceStatus = _evidenceStatus(base)
|
|
521
|
+
confidence = raw.get("confidence", payload.get("confidence", None))
|
|
522
|
+
if confidence is None:
|
|
523
|
+
confidence = _proofConfidence(base, openQuestions, escalation)
|
|
524
|
+
cardBase = {
|
|
525
|
+
"packId": packId,
|
|
526
|
+
"packHash": chainHash,
|
|
527
|
+
"title": str(raw.get("title", base.get("title", "Proof card")) or "Proof card"),
|
|
528
|
+
"summary": str(raw.get("summary", base.get("summary", "")) or ""),
|
|
529
|
+
"evidenceStatus": evidenceStatus,
|
|
530
|
+
"confidence": round(max(0.0, min(1.0, float(confidence or 0.0))), 3),
|
|
531
|
+
"openQuestions": openQuestions,
|
|
532
|
+
"recommendedRestraint": str(raw.get("recommendedRestraint", payload.get("recommendedRestraint", _restraint(openQuestions, escalation))) or ""),
|
|
533
|
+
"escalation": escalation,
|
|
534
|
+
}
|
|
535
|
+
cardHash = canonicalHash(cardBase)
|
|
536
|
+
cardId = "card_" + cardHash[:24]
|
|
537
|
+
return {
|
|
538
|
+
"cardId": cardId,
|
|
539
|
+
"kind": "proofCard",
|
|
540
|
+
**cardBase,
|
|
541
|
+
"cite": f"picux:proofPack:{packId}#{chainHash[:12]}",
|
|
542
|
+
"embed": {"href": f"/v1/audit/proofPacks/{packId}", "format": "application/json"},
|
|
543
|
+
"post": {
|
|
544
|
+
"title": cardBase["title"],
|
|
545
|
+
"summary": cardBase["summary"],
|
|
546
|
+
"evidenceStatus": evidenceStatus,
|
|
547
|
+
"confidence": cardBase["confidence"],
|
|
548
|
+
},
|
|
549
|
+
"cardHash": cardHash,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _evidenceStatus(base: dict[str, Any]) -> str:
|
|
554
|
+
if base.get("artifacts") and base.get("pov"):
|
|
555
|
+
return "verified"
|
|
556
|
+
if base.get("artifacts"):
|
|
557
|
+
return "sourceBound"
|
|
558
|
+
return "needsEvidence"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _proofConfidence(base: dict[str, Any], openQuestions: list[str], escalation: dict[str, Any]) -> float:
|
|
562
|
+
confidence = 0.35
|
|
563
|
+
if base.get("artifacts"):
|
|
564
|
+
confidence += 0.25
|
|
565
|
+
if base.get("pov"):
|
|
566
|
+
confidence += 0.20
|
|
567
|
+
if base.get("decisions"):
|
|
568
|
+
confidence += 0.10
|
|
569
|
+
if openQuestions:
|
|
570
|
+
confidence -= min(0.20, 0.05 * len(openQuestions))
|
|
571
|
+
if escalation.get("recommended"):
|
|
572
|
+
confidence -= 0.10
|
|
573
|
+
return confidence
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _restraint(openQuestions: list[str], escalation: dict[str, Any]) -> str:
|
|
577
|
+
if escalation.get("recommended"):
|
|
578
|
+
return "escalateBeforePublishing"
|
|
579
|
+
if openQuestions:
|
|
580
|
+
return "citeWithOpenQuestions"
|
|
581
|
+
return "shareableWithSource"
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _cleanDicts(value: Any) -> list[dict[str, Any]]:
|
|
585
|
+
if isinstance(value, dict):
|
|
586
|
+
return [dict(value)]
|
|
587
|
+
if not isinstance(value, list):
|
|
588
|
+
return []
|
|
589
|
+
return [dict(item) for item in value if isinstance(item, dict)]
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _cleanStrings(value: Any) -> list[str]:
|
|
593
|
+
if not isinstance(value, list):
|
|
594
|
+
return []
|
|
595
|
+
return [str(item) for item in value if str(item).strip()]
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _proofPackId(value: Any, chainHash: str) -> str:
|
|
599
|
+
raw = str(value or "")
|
|
600
|
+
if raw.startswith("pack_") and len(raw) == 29 and all(char in "0123456789abcdef" for char in raw[5:]):
|
|
601
|
+
return raw
|
|
602
|
+
return "pack_" + chainHash[:24]
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _povInputErrors(payload: dict[str, Any]) -> list[str]:
|
|
606
|
+
errors: list[str] = []
|
|
607
|
+
if not str(payload.get("taskId", "") or ""):
|
|
608
|
+
errors.append("missing:taskId")
|
|
609
|
+
if not str(payload.get("domain", "") or ""):
|
|
610
|
+
errors.append("missing:domain")
|
|
611
|
+
return errors
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _amount(value: Any) -> float | None:
|
|
615
|
+
try:
|
|
616
|
+
return float(value or 0.0)
|
|
617
|
+
except (TypeError, ValueError):
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _limit(value: Any) -> int:
|
|
622
|
+
try:
|
|
623
|
+
parsed = int(value)
|
|
624
|
+
except (TypeError, ValueError):
|
|
625
|
+
return 100
|
|
626
|
+
return max(1, min(parsed, 500))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _time(clock: Clock) -> str:
|
|
630
|
+
value = clock()
|
|
631
|
+
if isinstance(value, datetime):
|
|
632
|
+
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
633
|
+
return str(value)
|