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,144 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Any, Mapping
6
+
7
+
8
+ RAW_SECRET_KEYS = {"token", "apiKey", "secret", "password", "privateKey", "clientSecret", "value"}
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SecretResolution:
13
+ ref: str
14
+ kind: str
15
+ source: str
16
+ available: bool
17
+ errors: tuple[str, ...] = ()
18
+ value: str = ""
19
+
20
+ def toMap(self) -> dict[str, Any]:
21
+ return {
22
+ "ref": self.ref,
23
+ "kind": self.kind,
24
+ "source": self.source,
25
+ "available": self.available,
26
+ "errors": list(self.errors),
27
+ }
28
+
29
+
30
+ class SecretResolver:
31
+ """Resolve scoped secret references without serializing secret values."""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ env: Mapping[str, str] | None = None,
37
+ vault: Mapping[str, str] | None = None,
38
+ ) -> None:
39
+ self.env = os.environ if env is None else env
40
+ self.vault = vault or {}
41
+
42
+ def check(self, payload: dict[str, Any]) -> dict[str, Any]:
43
+ rawErrors = _rawSecretErrors(payload)
44
+ refs = self._refs(payload)
45
+ checks = [self.resolve(item["ref"], envName=item["env"], source=item["source"]) for item in refs]
46
+ errors = [*rawErrors]
47
+ if not refs:
48
+ errors.append("missing:ref")
49
+ for check in checks:
50
+ errors.extend(check.errors)
51
+ cleanErrors = list(dict.fromkeys(errors))
52
+ available = bool(checks) and all(check.available for check in checks)
53
+ return {
54
+ "ok": not cleanErrors and available,
55
+ "available": available and not rawErrors,
56
+ "count": len(checks),
57
+ "checks": [check.toMap() for check in checks],
58
+ "errors": cleanErrors,
59
+ }
60
+
61
+ def resolve(self, ref: str, *, envName: str = "", source: str = "") -> SecretResolution:
62
+ rawRef = str(ref or "").strip()
63
+ envName = str(envName or "").strip()
64
+ if not rawRef and envName:
65
+ rawRef = f"env:{envName}"
66
+ if not rawRef:
67
+ return SecretResolution(ref="", kind="unknown", source=source, available=False, errors=("missing:ref",))
68
+
69
+ kind, normalized = _kind(rawRef)
70
+ if kind == "env":
71
+ envKey = normalized
72
+ value = str(self.env.get(envKey, "") or "")
73
+ errors = () if value else (f"missingEnv:{envKey}",)
74
+ return SecretResolution(ref=f"env:{envKey}", kind="env", source=source, available=bool(value), errors=errors, value=value)
75
+
76
+ if kind == "vault":
77
+ value = str(self.vault.get(rawRef, "") or self.vault.get(normalized, "") or "")
78
+ if not value and envName:
79
+ value = str(self.env.get(envName, "") or "")
80
+ if value:
81
+ resolvedBy = "vault" if rawRef in self.vault or normalized in self.vault else f"env:{envName}"
82
+ return SecretResolution(ref=rawRef, kind="vault", source=source or resolvedBy, available=True, value=value)
83
+ return SecretResolution(ref=rawRef, kind="vault", source=source, available=False, errors=("vaultResolverMissing",))
84
+
85
+ return SecretResolution(ref=rawRef, kind="unknown", source=source, available=False, errors=("invalid:ref",))
86
+
87
+ def _refs(self, payload: dict[str, Any]) -> list[dict[str, str]]:
88
+ refs: list[dict[str, str]] = []
89
+
90
+ def add(ref: Any, *, envName: Any = "", source: str = "") -> None:
91
+ cleanRef = str(ref or "").strip()
92
+ cleanEnv = str(envName or "").strip()
93
+ if cleanRef or cleanEnv:
94
+ refs.append({"ref": cleanRef, "env": cleanEnv, "source": source})
95
+
96
+ add(payload.get("ref", ""), envName=payload.get("env", ""), source="payload")
97
+ for idx, ref in enumerate(payload.get("refs", []) if isinstance(payload.get("refs"), list) else []):
98
+ add(ref, source=f"refs.{idx}")
99
+
100
+ for source, item in _credentialItems(payload):
101
+ add(
102
+ item.get("tokenRef", item.get("secretRef", "")),
103
+ envName=item.get("env", ""),
104
+ source=source,
105
+ )
106
+ return refs
107
+
108
+
109
+ def _credentialItems(payload: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
110
+ items: list[tuple[str, dict[str, Any]]] = []
111
+ connector = payload.get("connector") if isinstance(payload.get("connector"), dict) else payload
112
+ credential = connector.get("credential", connector.get("auth", {})) if isinstance(connector, dict) else {}
113
+ if isinstance(credential, dict):
114
+ items.append(("connector.credential", credential))
115
+ scopes = connector.get("scopes", []) if isinstance(connector, dict) else []
116
+ if isinstance(scopes, list):
117
+ for idx, scope in enumerate(scopes):
118
+ if isinstance(scope, dict):
119
+ items.append((f"connector.scopes.{idx}", scope))
120
+ return items
121
+
122
+
123
+ def _kind(ref: str) -> tuple[str, str]:
124
+ if ref.startswith("env:"):
125
+ return "env", ref[4:]
126
+ if ref.startswith("vault:"):
127
+ return "vault", ref
128
+ if ref.isupper() and "_" in ref:
129
+ return "env", ref
130
+ return "unknown", ref
131
+
132
+
133
+ def _rawSecretErrors(value: Any, path: str = "payload") -> list[str]:
134
+ errors: list[str] = []
135
+ if isinstance(value, dict):
136
+ for key, item in value.items():
137
+ itemPath = f"{path}.{key}"
138
+ if key in RAW_SECRET_KEYS and str(item or "").strip():
139
+ errors.append(f"rawSecretNotAllowed:{itemPath}")
140
+ errors.extend(_rawSecretErrors(item, itemPath))
141
+ elif isinstance(value, list):
142
+ for idx, item in enumerate(value):
143
+ errors.extend(_rawSecretErrors(item, f"{path}.{idx}"))
144
+ return errors
@@ -0,0 +1 @@
1
+ """Signal ingestion, scoring, disambiguation, and adapter contracts."""
@@ -0,0 +1,24 @@
1
+ """Community signal harness primitives."""
2
+
3
+ from .disambiguation import classifySignalEntity, classify_signal_entity, inspectSignalEntity, isHarnessCollision, is_harness_collision
4
+ from .intake import CommunitySignalIntake
5
+ from .models import CommunitySignal, CommunitySignalSource, SignalAnalysis, SignalEntity
6
+ from .rules import CANONICAL_QUERIES, entityCatalog
7
+ from .scoring import scoreSignal, score_signal
8
+
9
+ __all__ = [
10
+ "CANONICAL_QUERIES",
11
+ "CommunitySignalIntake",
12
+ "CommunitySignal",
13
+ "CommunitySignalSource",
14
+ "SignalAnalysis",
15
+ "SignalEntity",
16
+ "classifySignalEntity",
17
+ "classify_signal_entity",
18
+ "entityCatalog",
19
+ "inspectSignalEntity",
20
+ "isHarnessCollision",
21
+ "is_harness_collision",
22
+ "scoreSignal",
23
+ "score_signal",
24
+ ]
@@ -0,0 +1,7 @@
1
+ """Community signal source adapters."""
2
+
3
+ from .reddit import fromReddit, fromRedditPost
4
+ from .shopify import fromShopify
5
+ from .web import fromWeb
6
+
7
+ __all__ = ["fromReddit", "fromRedditPost", "fromShopify", "fromWeb"]
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from picux.signals.community.models import CommunitySignal, CommunitySignalSource
7
+
8
+
9
+ def fromReddit(raw: dict[str, Any]) -> CommunitySignal:
10
+ title = str(raw.get("title", "") or "")
11
+ body = str(raw.get("selftext", raw.get("body", "")) or "")
12
+ permalink = str(raw.get("permalink", raw.get("url", "")) or "")
13
+ url = permalink if permalink.startswith("http") else f"https://www.reddit.com{permalink}" if permalink else ""
14
+ created = raw.get("createdAt", raw.get("created_utc", ""))
15
+ observedAt = _observedAt(created)
16
+ return CommunitySignal(
17
+ signalId=str(raw.get("signalId", raw.get("id", "")) or ""),
18
+ source=CommunitySignalSource(
19
+ platform="reddit",
20
+ url=url,
21
+ community=str(raw.get("subreddit", raw.get("community", "")) or ""),
22
+ metadata={"author": str(raw.get("author", "") or "")},
23
+ ),
24
+ observedAt=observedAt,
25
+ text=" ".join(part for part in (title, body) if part).strip(),
26
+ query=str(raw.get("query", "") or ""),
27
+ )
28
+
29
+
30
+ def fromRedditPost(raw: dict[str, Any]) -> CommunitySignal:
31
+ return fromReddit(raw)
32
+
33
+
34
+ def _observedAt(value: Any) -> str:
35
+ if isinstance(value, (int, float)):
36
+ return datetime.fromtimestamp(float(value), timezone.utc).isoformat().replace("+00:00", "Z")
37
+ return str(value or "")
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from picux.signals.community.models import CommunitySignal, CommunitySignalSource
6
+
7
+
8
+ def fromShopify(raw: dict[str, Any]) -> CommunitySignal:
9
+ topic = str(raw.get("topic", "") or "")
10
+ title = str(raw.get("title", raw.get("name", "")) or "")
11
+ body = str(raw.get("body", raw.get("description", "")) or "")
12
+ return CommunitySignal(
13
+ signalId=str(raw.get("signalId", raw.get("id", "")) or ""),
14
+ source=CommunitySignalSource(
15
+ platform="shopify",
16
+ url=str(raw.get("url", "") or ""),
17
+ community=str(raw.get("shop", "") or ""),
18
+ metadata={"topic": topic},
19
+ ),
20
+ observedAt=str(raw.get("observedAt", raw.get("createdAt", "")) or ""),
21
+ text=" ".join(part for part in (topic, title, body) if part).strip(),
22
+ query=str(raw.get("query", "") or ""),
23
+ )
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from picux.signals.community.models import CommunitySignal, CommunitySignalSource
6
+
7
+
8
+ def fromWeb(raw: dict[str, Any]) -> CommunitySignal:
9
+ title = str(raw.get("title", "") or "")
10
+ snippet = str(raw.get("snippet", "") or "")
11
+ body = str(raw.get("body", "") or "")
12
+ return CommunitySignal(
13
+ signalId=str(raw.get("signalId", raw.get("id", "")) or ""),
14
+ source=CommunitySignalSource(
15
+ platform="web",
16
+ url=str(raw.get("url", "") or ""),
17
+ community=str(raw.get("site", raw.get("domain", "")) or ""),
18
+ metadata={"rank": raw.get("rank", "")},
19
+ ),
20
+ observedAt=str(raw.get("observedAt", raw.get("capturedAt", "")) or ""),
21
+ text=" ".join(part for part in (title, snippet, body) if part).strip(),
22
+ query=str(raw.get("query", "") or ""),
23
+ )
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from picux.signals.community.models import SignalEntity
6
+ from picux.signals.community.rules import ENTITY_RULES, phraseHits
7
+
8
+
9
+ ENTITY_PRIORITY = (
10
+ SignalEntity.PICUX,
11
+ SignalEntity.EXTERNAL_APP,
12
+ SignalEntity.HARNESS,
13
+ SignalEntity.LEGACY_SWARM,
14
+ )
15
+
16
+
17
+ def inspectSignalEntity(text: str) -> dict[str, Any]:
18
+ matchesByEntity: dict[SignalEntity, tuple[str, ...]] = {
19
+ rule.entity: phraseHits(text, rule.aliases) for rule in ENTITY_RULES
20
+ }
21
+ entity = SignalEntity.UNKNOWN
22
+ for candidate in ENTITY_PRIORITY:
23
+ if matchesByEntity.get(candidate):
24
+ entity = candidate
25
+ break
26
+
27
+ harnessMatches = matchesByEntity.get(SignalEntity.HARNESS, ())
28
+ return {
29
+ "entity": entity,
30
+ "matches": matchesByEntity.get(entity, ()),
31
+ "matchesByEntity": {key.value: list(value) for key, value in matchesByEntity.items() if value},
32
+ "harnessMatches": harnessMatches,
33
+ "harnessCollision": bool(harnessMatches),
34
+ }
35
+
36
+
37
+ def classifySignalEntity(text: str) -> SignalEntity:
38
+ return inspectSignalEntity(text)["entity"]
39
+
40
+
41
+ def isHarnessCollision(text: str) -> bool:
42
+ detail = inspectSignalEntity(text)
43
+ return bool(detail["harnessCollision"])
44
+
45
+
46
+ def classify_signal_entity(text: str) -> SignalEntity:
47
+ return classifySignalEntity(text)
48
+
49
+
50
+ def is_harness_collision(text: str) -> bool:
51
+ return isHarnessCollision(text)
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Callable
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from picux.core.models import Domain, ProtocolTask, ProtocolTaskStatus
10
+ from picux.signals.community.models import CommunitySignal, CommunitySignalSource, SignalEntity
11
+ from picux.signals.community.rules import LAUNCH_DOMAINS, entityCatalog
12
+ from picux.signals.community.scoring import scoreSignal
13
+ from picux.storage.signals import SignalBook
14
+
15
+
16
+ MISINFORMATION_REVIEW_THRESHOLD = 0.35
17
+
18
+
19
+ class CommunitySignalIntake:
20
+ """Deterministic intake surface for public community signals."""
21
+
22
+ def __init__(self, *, clock: Callable[[], datetime] | None = None, signalBook: SignalBook | None = None) -> None:
23
+ self.clock = clock or (lambda: datetime.now(timezone.utc))
24
+ self.signalBook = signalBook or SignalBook()
25
+
26
+ def makeSignal(self, raw: CommunitySignal | dict[str, Any]) -> CommunitySignal:
27
+ if isinstance(raw, CommunitySignal):
28
+ signal = raw
29
+ else:
30
+ source = CommunitySignalSource.fromObj(raw.get("source") if isinstance(raw.get("source"), dict) else {})
31
+ signal = CommunitySignal(
32
+ signalId=str(raw.get("signalId", "") or ""),
33
+ source=source,
34
+ observedAt=str(raw.get("observedAt", "") or ""),
35
+ text=str(raw.get("text", "") or ""),
36
+ query=str(raw.get("query", "") or ""),
37
+ )
38
+ observedAt = signal.observedAt or self.clock().isoformat().replace("+00:00", "Z")
39
+ signalId = signal.signalId or self._stableId(signal.source, observedAt, signal.text, signal.query)
40
+ analysis = scoreSignal(
41
+ CommunitySignal(
42
+ signalId=signalId,
43
+ source=signal.source,
44
+ observedAt=observedAt,
45
+ text=signal.text,
46
+ query=signal.query,
47
+ )
48
+ )
49
+ return CommunitySignal(
50
+ signalId=signalId,
51
+ source=signal.source,
52
+ observedAt=observedAt,
53
+ text=signal.text,
54
+ query=signal.query,
55
+ entity=analysis.entity,
56
+ confidence=analysis.confidence,
57
+ )
58
+
59
+ def ingest(self, raw: CommunitySignal | dict[str, Any]) -> dict[str, Any]:
60
+ signal = self.makeSignal(raw)
61
+ analysis = scoreSignal(signal)
62
+ saved = self.signalBook.saveSignal(signal, analysis)
63
+ result = {
64
+ "ok": True,
65
+ "signal": signal.toMap(),
66
+ "analysis": analysis.toMap(),
67
+ "humanReview": self._misinformationEscalation(Domain.HUNT.value, analysis),
68
+ }
69
+ if saved.get("ok"):
70
+ result["record"] = saved["record"]
71
+ return result
72
+
73
+ def analyze(self, raw: CommunitySignal | dict[str, Any]) -> dict[str, Any]:
74
+ signal = self.makeSignal(raw)
75
+ analysis = scoreSignal(signal)
76
+ return {**analysis.toMap(), "humanReview": self._misinformationEscalation(Domain.HUNT.value, analysis)}
77
+
78
+ def getSignal(self, signalId: str) -> dict[str, Any]:
79
+ return self.signalBook.getSignal(signalId)
80
+
81
+ def listSignals(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
82
+ return self.signalBook.listSignals(filters)
83
+
84
+ def entities(self) -> dict[str, Any]:
85
+ return {"ok": True, "entities": entityCatalog()}
86
+
87
+ def launchTask(self, raw: dict[str, Any]) -> dict[str, Any]:
88
+ target = str(raw.get("targetDomain", raw.get("domain", Domain.HUNT.value)) or Domain.HUNT.value).lower()
89
+ if target not in LAUNCH_DOMAINS:
90
+ return {"ok": False, "error": "domainNotLaunchable", "launchableDomains": list(LAUNCH_DOMAINS)}
91
+
92
+ signalRaw = raw.get("signal") if isinstance(raw.get("signal"), dict) else raw
93
+ signal = self.makeSignal(signalRaw)
94
+ analysis = scoreSignal(signal)
95
+ saved = self.signalBook.saveSignal(signal, analysis)
96
+ record = saved.get("record") if saved.get("ok") else None
97
+ if analysis.entity == SignalEntity.HARNESS:
98
+ result = {"ok": False, "error": "harnessCollision", "analysis": analysis.toMap()}
99
+ if record:
100
+ result["record"] = record
101
+ return result
102
+ escalation = self._misinformationEscalation(target, analysis)
103
+ if escalation["required"]:
104
+ result = self._proxyEscalationTask(raw, signal, analysis, escalation)
105
+ if record:
106
+ result["record"] = record
107
+ return result
108
+ if target not in analysis.launchableDomains:
109
+ result = {"ok": False, "error": "signalNotLaunchable", "analysis": analysis.toMap()}
110
+ if record:
111
+ result["record"] = record
112
+ return result
113
+
114
+ approved = bool(raw.get("approved", False))
115
+ needsApproval = not approved
116
+ status = ProtocolTaskStatus.NEEDS_APPROVAL if needsApproval else ProtocolTaskStatus.PENDING
117
+ task = ProtocolTask(
118
+ taskId=self._taskId(signal.signalId, target),
119
+ userId=str(raw.get("userId", "community") or "community"),
120
+ domain=Domain(target),
121
+ status=status,
122
+ channel="community",
123
+ inData={"signal": signal.toMap(), "analysis": analysis.toMap()},
124
+ needsApproval=needsApproval,
125
+ extRef=signal.signalId,
126
+ meta={"source": "communitySignal", "approvalGate": "required" if needsApproval else "approved"},
127
+ )
128
+ result = {
129
+ "ok": True,
130
+ "approvalRequired": needsApproval,
131
+ "analysis": analysis.toMap(),
132
+ "task": {
133
+ "taskId": task.taskId,
134
+ "userId": task.userId,
135
+ "domain": task.domain.value,
136
+ "status": task.status.value,
137
+ "channel": task.channel,
138
+ "inData": task.inData,
139
+ "needsApproval": task.needsApproval,
140
+ "extRef": task.extRef,
141
+ "meta": task.meta,
142
+ },
143
+ }
144
+ if record:
145
+ result["record"] = record
146
+ return result
147
+
148
+ def _proxyEscalationTask(
149
+ self,
150
+ raw: dict[str, Any],
151
+ signal: CommunitySignal,
152
+ analysis: Any,
153
+ escalation: dict[str, Any],
154
+ ) -> dict[str, Any]:
155
+ target = str(escalation.get("proposedDomain", Domain.HUNT.value) or Domain.HUNT.value)
156
+ task = ProtocolTask(
157
+ taskId=self._taskId(signal.signalId, "proxy"),
158
+ userId=str(raw.get("userId", "community") or "community"),
159
+ domain=Domain.PROXY,
160
+ status=ProtocolTaskStatus.NEEDS_APPROVAL,
161
+ channel="community",
162
+ inData={"signal": signal.toMap(), "analysis": analysis.toMap(), "escalation": escalation},
163
+ needsApproval=True,
164
+ extRef=signal.signalId,
165
+ meta={
166
+ "source": "communitySignal",
167
+ "approvalGate": "required",
168
+ "misinformationEscalation": "required",
169
+ "proposedDomain": target,
170
+ },
171
+ )
172
+ return {
173
+ "ok": True,
174
+ "approvalRequired": True,
175
+ "escalationRequired": True,
176
+ "analysis": analysis.toMap(),
177
+ "humanReview": escalation,
178
+ "task": {
179
+ "taskId": task.taskId,
180
+ "userId": task.userId,
181
+ "domain": task.domain.value,
182
+ "status": task.status.value,
183
+ "channel": task.channel,
184
+ "inData": task.inData,
185
+ "needsApproval": task.needsApproval,
186
+ "extRef": task.extRef,
187
+ "meta": task.meta,
188
+ },
189
+ }
190
+
191
+ @staticmethod
192
+ def _misinformationEscalation(targetDomain: str, analysis: Any) -> dict[str, Any]:
193
+ reasons: list[str] = []
194
+ if float(getattr(analysis, "confidence", 0.0) or 0.0) < MISINFORMATION_REVIEW_THRESHOLD:
195
+ reasons.append("lowConfidence")
196
+ if getattr(analysis, "entity", SignalEntity.UNKNOWN) == SignalEntity.UNKNOWN:
197
+ reasons.append("unknownEntity")
198
+ launchable = tuple(getattr(analysis, "launchableDomains", ()) or ())
199
+ if targetDomain not in launchable:
200
+ reasons.append("notLaunchable")
201
+ required = bool(reasons)
202
+ return {
203
+ "required": required,
204
+ "routeDomain": Domain.PROXY.value if required else "",
205
+ "proposedDomain": str(targetDomain or ""),
206
+ "threshold": MISINFORMATION_REVIEW_THRESHOLD,
207
+ "confidence": float(getattr(analysis, "confidence", 0.0) or 0.0),
208
+ "reasons": list(dict.fromkeys(reasons)),
209
+ "protocol": "misinformationEscalation",
210
+ "instruction": "Route to human review before surfacing as an answer." if required else "",
211
+ }
212
+
213
+ @staticmethod
214
+ def _stableId(source: CommunitySignalSource, observedAt: str, text: str, query: str) -> str:
215
+ payload = {
216
+ "source": source.toMap(),
217
+ "observedAt": observedAt,
218
+ "text": text,
219
+ "query": query,
220
+ }
221
+ digest = hashlib.sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True).encode("utf-8")).hexdigest()
222
+ return f"sig_{digest[:24]}"
223
+
224
+ @staticmethod
225
+ def _taskId(signalId: str, domain: str) -> str:
226
+ digest = hashlib.sha256(f"{signalId}:{domain}".encode("utf-8")).hexdigest()
227
+ return f"task_{digest[:24]}"
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class SignalEntity(str, Enum):
9
+ PICUX = "picux"
10
+ EXTERNAL_APP = "external_app"
11
+ LEGACY_SWARM = "legacy_swarm"
12
+ HARNESS = "harness"
13
+ UNKNOWN = "unknown"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class CommunitySignalSource:
18
+ platform: str
19
+ url: str = ""
20
+ community: str = ""
21
+ metadata: dict[str, Any] = field(default_factory=dict)
22
+
23
+ @classmethod
24
+ def fromObj(cls, value: dict[str, Any] | None) -> "CommunitySignalSource":
25
+ value = value or {}
26
+ return cls(
27
+ platform=str(value.get("platform", "") or ""),
28
+ url=str(value.get("url", "") or ""),
29
+ community=str(value.get("community", "") or ""),
30
+ metadata=value.get("metadata", {}) if isinstance(value.get("metadata", {}), dict) else {},
31
+ )
32
+
33
+ def toMap(self) -> dict[str, Any]:
34
+ return {
35
+ "platform": self.platform,
36
+ "url": self.url,
37
+ "community": self.community,
38
+ "metadata": self.metadata,
39
+ }
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class CommunitySignal:
44
+ signalId: str
45
+ source: CommunitySignalSource
46
+ observedAt: str
47
+ text: str
48
+ query: str = ""
49
+ entity: SignalEntity = SignalEntity.UNKNOWN
50
+ confidence: float = 0.0
51
+
52
+ @classmethod
53
+ def fromObj(cls, value: dict[str, Any]) -> "CommunitySignal":
54
+ entity = str(value.get("entity", SignalEntity.UNKNOWN.value) or SignalEntity.UNKNOWN.value)
55
+ return cls(
56
+ signalId=str(value.get("signalId", "") or ""),
57
+ source=CommunitySignalSource.fromObj(value.get("source") if isinstance(value.get("source"), dict) else {}),
58
+ observedAt=str(value.get("observedAt", "") or ""),
59
+ text=str(value.get("text", "") or ""),
60
+ query=str(value.get("query", "") or ""),
61
+ entity=SignalEntity(entity) if entity in SignalEntity._value2member_map_ else SignalEntity.UNKNOWN,
62
+ confidence=float(value.get("confidence", 0.0) or 0.0),
63
+ )
64
+
65
+ def toMap(self) -> dict[str, Any]:
66
+ return {
67
+ "signalId": self.signalId,
68
+ "source": self.source.toMap(),
69
+ "observedAt": self.observedAt,
70
+ "text": self.text,
71
+ "query": self.query,
72
+ "entity": self.entity.value,
73
+ "confidence": self.confidence,
74
+ }
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class SignalAnalysis:
79
+ signalId: str
80
+ entity: SignalEntity
81
+ confidence: float
82
+ matches: tuple[str, ...] = ()
83
+ negativeMatches: tuple[str, ...] = ()
84
+ reasons: tuple[str, ...] = ()
85
+ query: str = ""
86
+ source: CommunitySignalSource = field(default_factory=lambda: CommunitySignalSource(platform=""))
87
+ launchableDomains: tuple[str, ...] = ()
88
+ approvalRequired: bool = True
89
+
90
+ def toMap(self) -> dict[str, Any]:
91
+ return {
92
+ "signalId": self.signalId,
93
+ "entity": self.entity.value,
94
+ "confidence": self.confidence,
95
+ "matches": list(self.matches),
96
+ "negativeMatches": list(self.negativeMatches),
97
+ "reasons": list(self.reasons),
98
+ "query": self.query,
99
+ "source": self.source.toMap(),
100
+ "launchableDomains": list(self.launchableDomains),
101
+ "approvalRequired": self.approvalRequired,
102
+ }