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,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,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
|
+
}
|