hypermind 0.11.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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Sigstore Rekor read-only client + downgrade-mode submitter.
|
|
2
|
+
|
|
3
|
+
ROADMAP §0.3.1 (read), §0.4.3 (write).
|
|
4
|
+
|
|
5
|
+
The client is intentionally minimal: it speaks the Rekor v1 REST API
|
|
6
|
+
and handles transparency-log entry lookup + RFC 6962 inclusion-proof
|
|
7
|
+
verification. Any heavier integration (key trust roots, fulcio cert
|
|
8
|
+
chain validation) is deferred to v0.7 alongside the rest of the
|
|
9
|
+
sigstore trust-bundle work.
|
|
10
|
+
|
|
11
|
+
``httpx`` is a soft dependency — imports are lazy so the SDK still
|
|
12
|
+
loads in environments that haven't installed the ``[rekor]`` extra.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from hypermind.knowledge.transparency import (
|
|
23
|
+
PortableInclusionProof,
|
|
24
|
+
verify_inclusion,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RekorUnavailableError(RuntimeError):
|
|
29
|
+
"""Raised when ``httpx`` is not installed."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_httpx() -> Any:
|
|
33
|
+
try:
|
|
34
|
+
import httpx
|
|
35
|
+
except ImportError as e: # pragma: no cover - depends on env
|
|
36
|
+
raise RekorUnavailableError(
|
|
37
|
+
"Rekor integration requires `httpx`. "
|
|
38
|
+
"Install with `pip install httpx` or `pip install 'hypermind[rekor]'`."
|
|
39
|
+
) from e
|
|
40
|
+
return httpx
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class RekorEntry:
|
|
45
|
+
"""A single Rekor log entry, normalised across upstream API shapes."""
|
|
46
|
+
|
|
47
|
+
uuid: str
|
|
48
|
+
log_index: int
|
|
49
|
+
integrated_time: int
|
|
50
|
+
log_id: str
|
|
51
|
+
body: bytes # base64-decoded body (the canonical entry blob)
|
|
52
|
+
inclusion_proof: PortableInclusionProof | None
|
|
53
|
+
root_hash: bytes | None
|
|
54
|
+
tree_size: int | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class RekorClient:
|
|
59
|
+
"""Read-only Sigstore Rekor client.
|
|
60
|
+
|
|
61
|
+
Construct with the public Rekor URL or a self-hosted instance::
|
|
62
|
+
|
|
63
|
+
client = RekorClient("https://rekor.sigstore.dev")
|
|
64
|
+
entry = client.lookup("e3b0c44...uuid...")
|
|
65
|
+
assert client.verify_inclusion(entry)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
base_url: str = "https://rekor.sigstore.dev"
|
|
69
|
+
timeout: float = 5.0
|
|
70
|
+
transport: Any = None # httpx.BaseTransport — for tests
|
|
71
|
+
|
|
72
|
+
def _client(self) -> Any:
|
|
73
|
+
httpx = _load_httpx()
|
|
74
|
+
kwargs: dict[str, Any] = {"base_url": self.base_url, "timeout": self.timeout}
|
|
75
|
+
if self.transport is not None:
|
|
76
|
+
kwargs["transport"] = self.transport
|
|
77
|
+
return httpx.Client(**kwargs)
|
|
78
|
+
|
|
79
|
+
# ---- read paths --------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def lookup(self, uuid: str) -> RekorEntry:
|
|
82
|
+
"""Fetch and parse a single Rekor entry by UUID."""
|
|
83
|
+
with self._client() as c:
|
|
84
|
+
resp = c.get(f"/api/v1/log/entries/{uuid}")
|
|
85
|
+
resp.raise_for_status()
|
|
86
|
+
body = resp.json()
|
|
87
|
+
return _parse_entry(body)
|
|
88
|
+
|
|
89
|
+
def verify_inclusion(self, entry: RekorEntry) -> bool:
|
|
90
|
+
"""Verify the entry's inclusion proof against its embedded root.
|
|
91
|
+
|
|
92
|
+
Returns False (not raises) on missing or malformed proof so
|
|
93
|
+
callers can treat unverifiable entries as untrusted.
|
|
94
|
+
"""
|
|
95
|
+
if entry.inclusion_proof is None or entry.root_hash is None:
|
|
96
|
+
return False
|
|
97
|
+
leaf_hash = hashlib.sha256(b"\x00" + entry.body).digest()
|
|
98
|
+
return verify_inclusion(leaf_hash, entry.inclusion_proof, entry.root_hash)
|
|
99
|
+
|
|
100
|
+
# ---- write paths (ROADMAP §0.4.3) --------------------------------
|
|
101
|
+
|
|
102
|
+
def submit_downgrade(self, signed_statement: Any) -> str:
|
|
103
|
+
"""Submit a HyperMind ``SignedStatement`` to Rekor as cross-anchor
|
|
104
|
+
evidence.
|
|
105
|
+
|
|
106
|
+
Used when an operator wants Sigstore-anchored proof that a
|
|
107
|
+
statement existed at the submission time. Returns the Rekor UUID
|
|
108
|
+
of the new entry. The "downgrade" terminology reflects that the
|
|
109
|
+
HyperMind log is the canonical primary source — Rekor is a
|
|
110
|
+
secondary anchor used for cross-ecosystem auditability.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
payload = signed_statement.to_bytes()
|
|
114
|
+
except AttributeError:
|
|
115
|
+
payload = bytes(signed_statement)
|
|
116
|
+
body_b64 = base64.b64encode(payload).decode("ascii")
|
|
117
|
+
proposed = {
|
|
118
|
+
"kind": "hypermind",
|
|
119
|
+
"apiVersion": "0.0.1",
|
|
120
|
+
"spec": {"content": body_b64},
|
|
121
|
+
}
|
|
122
|
+
with self._client() as c:
|
|
123
|
+
resp = c.post("/api/v1/log/entries", json=proposed)
|
|
124
|
+
resp.raise_for_status()
|
|
125
|
+
data = resp.json()
|
|
126
|
+
# Rekor returns {uuid: entry_dict}
|
|
127
|
+
if isinstance(data, dict) and len(data) == 1:
|
|
128
|
+
return next(iter(data.keys()))
|
|
129
|
+
raise RuntimeError(f"unexpected Rekor response: {data!r}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_entry(body: dict[str, Any]) -> RekorEntry:
|
|
133
|
+
"""Normalise a Rekor v1 entries response into a :class:`RekorEntry`.
|
|
134
|
+
|
|
135
|
+
Rekor returns ``{uuid: {body, integratedTime, logIndex, ...}}``; we
|
|
136
|
+
accept either the wrapped form or a single-entry dict.
|
|
137
|
+
"""
|
|
138
|
+
if not isinstance(body, dict):
|
|
139
|
+
raise ValueError(f"unexpected entry shape: {type(body)}")
|
|
140
|
+
if len(body) == 1 and isinstance(next(iter(body.values())), dict):
|
|
141
|
+
uuid, raw = next(iter(body.items()))
|
|
142
|
+
else:
|
|
143
|
+
uuid = body.get("uuid", "")
|
|
144
|
+
raw = body
|
|
145
|
+
inc = raw.get("verification", {}).get("inclusionProof", {}) or {}
|
|
146
|
+
proof: PortableInclusionProof | None = None
|
|
147
|
+
root_hash: bytes | None = None
|
|
148
|
+
tree_size: int | None = None
|
|
149
|
+
if inc:
|
|
150
|
+
try:
|
|
151
|
+
audit_path = tuple(bytes.fromhex(h) for h in inc.get("hashes", []))
|
|
152
|
+
leaf_index = int(inc["logIndex"])
|
|
153
|
+
tree_size = int(inc["treeSize"])
|
|
154
|
+
root_hash = bytes.fromhex(inc["rootHash"])
|
|
155
|
+
proof = PortableInclusionProof(
|
|
156
|
+
audit_path=audit_path,
|
|
157
|
+
leaf_index=leaf_index,
|
|
158
|
+
tree_size=tree_size,
|
|
159
|
+
)
|
|
160
|
+
except (KeyError, ValueError):
|
|
161
|
+
proof = None
|
|
162
|
+
body_b64 = raw.get("body", "")
|
|
163
|
+
body_bytes = base64.b64decode(body_b64) if body_b64 else b""
|
|
164
|
+
return RekorEntry(
|
|
165
|
+
uuid=uuid,
|
|
166
|
+
log_index=int(raw.get("logIndex", 0)),
|
|
167
|
+
integrated_time=int(raw.get("integratedTime", 0)),
|
|
168
|
+
log_id=str(raw.get("logID", "")),
|
|
169
|
+
body=body_bytes,
|
|
170
|
+
inclusion_proof=proof,
|
|
171
|
+
root_hash=root_hash,
|
|
172
|
+
tree_size=tree_size,
|
|
173
|
+
)
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Reputation kernel.
|
|
2
|
+
|
|
3
|
+
v0.1 ships per-(agent, topic) reputation with smooth slashing and lazy
|
|
4
|
+
decay. v0.2 will swap the elicitation/aggregation rule for Correlated
|
|
5
|
+
Agreement peer-prediction (Wei-Chen Cycle 2 §1) without touching this
|
|
6
|
+
interface.
|
|
7
|
+
|
|
8
|
+
Reputation is stored as a moving Beta posterior:
|
|
9
|
+
- α = correct outcomes + 1 (Laplace prior)
|
|
10
|
+
- β = incorrect outcomes + 1
|
|
11
|
+
- mean = α / (α + β) ∈ (0, 1)
|
|
12
|
+
|
|
13
|
+
Slashing: α' = α · (1 - λ), β' = β + μ on a confirmed bad outcome
|
|
14
|
+
Decay: half-life H_T (default 180d) on the (α + β - 2) above-prior mass
|
|
15
|
+
|
|
16
|
+
Wire-version-0 normative defaults (closes spec/06-conformance.md gap #13):
|
|
17
|
+
λ = 0.02, μ = 0.015, threshold = 0.5, H_T = 180 days
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import math
|
|
24
|
+
import time
|
|
25
|
+
from collections import deque
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from hypermind.transport.bus import GossipBus
|
|
31
|
+
|
|
32
|
+
LAMBDA_DEFAULT = 0.02
|
|
33
|
+
MU_DEFAULT = 0.015
|
|
34
|
+
THRESHOLD_DEFAULT = 0.5
|
|
35
|
+
H_T_DEFAULT_DAYS = 180
|
|
36
|
+
STALENESS_WINDOW_MS_DEFAULT = 90 * 24 * 60 * 60 * 1000 # 90 days
|
|
37
|
+
LAMBDA_STALENESS_DEFAULT = 0.005
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ReputationSnapshot:
|
|
42
|
+
agent_id: bytes
|
|
43
|
+
topic: str
|
|
44
|
+
score: float
|
|
45
|
+
n_observations: int
|
|
46
|
+
last_updated_ms: int
|
|
47
|
+
momentum_z: float = 0.0 # A-MOMENTUM-DETECTION
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class _BetaPosterior:
|
|
52
|
+
alpha: float = 1.0
|
|
53
|
+
beta: float = 1.0
|
|
54
|
+
last_update_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
55
|
+
last_score: float = 0.5
|
|
56
|
+
score_history: deque = field(default_factory=lambda: deque(maxlen=200))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReputationKernel:
|
|
60
|
+
"""Per-(agent, topic) reputation with bounded slashing + lazy decay."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
lam: float = LAMBDA_DEFAULT,
|
|
66
|
+
mu: float = MU_DEFAULT,
|
|
67
|
+
threshold: float = THRESHOLD_DEFAULT,
|
|
68
|
+
half_life_days: int = H_T_DEFAULT_DAYS,
|
|
69
|
+
staleness_window_ms: int = STALENESS_WINDOW_MS_DEFAULT,
|
|
70
|
+
lambda_staleness: float = LAMBDA_STALENESS_DEFAULT,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.lam = lam
|
|
73
|
+
self.mu = mu
|
|
74
|
+
self.threshold = threshold
|
|
75
|
+
self.half_life_days = half_life_days
|
|
76
|
+
self.staleness_window_ms = staleness_window_ms
|
|
77
|
+
self.lambda_staleness = lambda_staleness
|
|
78
|
+
self._posteriors: dict[tuple[bytes, str], _BetaPosterior] = {}
|
|
79
|
+
# v0.6 — closed-loop calibration. Lazy-init to avoid pulling
|
|
80
|
+
# eval/ at import time of this file.
|
|
81
|
+
self._brier_tracker = None # type: ignore[assignment]
|
|
82
|
+
# P2-16: optional bus attachment for KNOWS_DELTA broadcasting.
|
|
83
|
+
self._bus: GossipBus | None = None
|
|
84
|
+
self._bus_issuer_kid: bytes = b""
|
|
85
|
+
self._bus_namespace_id: str = ""
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def brier_tracker(self):
|
|
89
|
+
"""Per-(agent, topic) Brier-score tracker.
|
|
90
|
+
|
|
91
|
+
Lazy-instantiated on first access. Lives for the kernel's
|
|
92
|
+
lifetime; mirrors to disk via ``ReputationKernel.snapshot()``
|
|
93
|
+
future work in the persistence layer (v0.6.5).
|
|
94
|
+
"""
|
|
95
|
+
if self._brier_tracker is None:
|
|
96
|
+
from hypermind.eval.calibration import BrierScoreTracker
|
|
97
|
+
|
|
98
|
+
self._brier_tracker = BrierScoreTracker()
|
|
99
|
+
return self._brier_tracker
|
|
100
|
+
|
|
101
|
+
def calibration_adjustment(self, agent_id: bytes, topic: str) -> float:
|
|
102
|
+
"""Return the multiplicative ``effective_n`` shrinkage for this agent.
|
|
103
|
+
|
|
104
|
+
Returns 1.0 (no-op) when there are too few oracle resolutions to
|
|
105
|
+
compute a meaningful Brier score. Otherwise ∈ [0.5, 2.0].
|
|
106
|
+
"""
|
|
107
|
+
return self.brier_tracker.adjustment(agent_id, topic)
|
|
108
|
+
|
|
109
|
+
# P2-16: bus attachment --------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def attach_bus(
|
|
112
|
+
self,
|
|
113
|
+
bus: GossipBus,
|
|
114
|
+
*,
|
|
115
|
+
issuer_kid: bytes,
|
|
116
|
+
namespace_id: str,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Attach a GossipBus so reputation deltas are broadcast as KNOWS_DELTA.
|
|
119
|
+
|
|
120
|
+
Call once after constructing the kernel. ``issuer_kid`` must be the
|
|
121
|
+
32-byte Ed25519 kid of the agent/namespace that owns this kernel
|
|
122
|
+
(used to sign outbound KNOWS_DELTA messages).
|
|
123
|
+
|
|
124
|
+
Note: publication requires a signing keypair to produce verifiable
|
|
125
|
+
messages. For lightweight delta broadcast (no signature), pass an
|
|
126
|
+
unsigned message. Full signed broadcast requires ``attach_bus_with_keypair``.
|
|
127
|
+
"""
|
|
128
|
+
self._bus = bus
|
|
129
|
+
self._bus_issuer_kid = bytes(issuer_kid)
|
|
130
|
+
self._bus_namespace_id = namespace_id
|
|
131
|
+
|
|
132
|
+
def attach_bus_with_keypair(
|
|
133
|
+
self,
|
|
134
|
+
bus: GossipBus,
|
|
135
|
+
keypair: object, # Keypair — avoid import cycle; duck-typed
|
|
136
|
+
*,
|
|
137
|
+
namespace_id: str,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Attach a GossipBus with a signing keypair for signed KNOWS_DELTA.
|
|
140
|
+
|
|
141
|
+
``keypair`` must be a :class:`~hypermind.crypto.signing.Keypair`
|
|
142
|
+
instance. Use this variant when you want fully verified broadcasts.
|
|
143
|
+
"""
|
|
144
|
+
self._bus = bus
|
|
145
|
+
self._bus_issuer_kid = bytes(keypair.kid) # type: ignore[union-attr]
|
|
146
|
+
self._bus_namespace_id = namespace_id
|
|
147
|
+
self._bus_keypair = keypair # type: ignore[assignment]
|
|
148
|
+
|
|
149
|
+
def _schedule_knows_delta_broadcast(
|
|
150
|
+
self,
|
|
151
|
+
agent_id: bytes,
|
|
152
|
+
topic: str,
|
|
153
|
+
alpha_delta: float,
|
|
154
|
+
beta_delta: float,
|
|
155
|
+
evidence_kid: bytes | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Fire-and-forget KNOWS_DELTA broadcast after a reputation update.
|
|
158
|
+
|
|
159
|
+
Uses ``asyncio.create_task`` so synchronous callers (``record_outcome``,
|
|
160
|
+
``vouch``) can trigger the async broadcast without blocking. Silently
|
|
161
|
+
suppresses errors when no event loop is running (e.g. in synchronous
|
|
162
|
+
test contexts that don't need the broadcast).
|
|
163
|
+
"""
|
|
164
|
+
if self._bus is None:
|
|
165
|
+
return
|
|
166
|
+
keypair = getattr(self, "_bus_keypair", None)
|
|
167
|
+
if keypair is None:
|
|
168
|
+
return # unsigned broadcasts not implemented — skip
|
|
169
|
+
bus = self._bus
|
|
170
|
+
namespace_id = self._bus_namespace_id
|
|
171
|
+
hlc = int(time.time() * 1000)
|
|
172
|
+
|
|
173
|
+
async def _broadcast() -> None:
|
|
174
|
+
from hypermind.transport.knows_delta import (
|
|
175
|
+
KnowsDeltaBucket,
|
|
176
|
+
KnowsDeltaMessage,
|
|
177
|
+
publish_knows_delta,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
bucket = KnowsDeltaBucket(
|
|
181
|
+
topic_id=topic,
|
|
182
|
+
subject_kid=bytes(agent_id),
|
|
183
|
+
alpha_delta=alpha_delta,
|
|
184
|
+
beta_delta=beta_delta,
|
|
185
|
+
evidence_kid=evidence_kid,
|
|
186
|
+
)
|
|
187
|
+
msg = KnowsDeltaMessage.create(
|
|
188
|
+
keypair=keypair,
|
|
189
|
+
namespace_id=namespace_id,
|
|
190
|
+
hlc=hlc,
|
|
191
|
+
buckets=[bucket],
|
|
192
|
+
)
|
|
193
|
+
try:
|
|
194
|
+
await publish_knows_delta(msg, bus)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass # best-effort
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
loop = asyncio.get_running_loop()
|
|
200
|
+
_task = loop.create_task(_broadcast()) # fire-and-forget; reference kept to avoid GC
|
|
201
|
+
del _task # suppress RUF006: intentionally not awaited
|
|
202
|
+
except RuntimeError:
|
|
203
|
+
pass # no running event loop — skip broadcast
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _staleness_factor(
|
|
207
|
+
evidence_hlc_ms: int,
|
|
208
|
+
now_hlc_ms: int,
|
|
209
|
+
*,
|
|
210
|
+
staleness_window_ms: int = STALENESS_WINDOW_MS_DEFAULT,
|
|
211
|
+
lambda_staleness: float = LAMBDA_STALENESS_DEFAULT,
|
|
212
|
+
) -> float:
|
|
213
|
+
"""Return a weight in (0, 1] based on how old the evidence is.
|
|
214
|
+
|
|
215
|
+
Evidence within ``staleness_window_ms`` of now has full weight 1.0.
|
|
216
|
+
Evidence older than the window decays exponentially with rate
|
|
217
|
+
``lambda_staleness`` per day.
|
|
218
|
+
"""
|
|
219
|
+
age_ms = now_hlc_ms - evidence_hlc_ms
|
|
220
|
+
if age_ms <= staleness_window_ms:
|
|
221
|
+
return 1.0
|
|
222
|
+
excess_days = (age_ms - staleness_window_ms) / 86_400_000
|
|
223
|
+
return math.exp(-lambda_staleness * excess_days)
|
|
224
|
+
|
|
225
|
+
def _get(self, agent_id: bytes, topic: str) -> _BetaPosterior:
|
|
226
|
+
key = (bytes(agent_id), topic)
|
|
227
|
+
if key not in self._posteriors:
|
|
228
|
+
self._posteriors[key] = _BetaPosterior()
|
|
229
|
+
return self._posteriors[key]
|
|
230
|
+
|
|
231
|
+
def _decay(self, p: _BetaPosterior, *, now_ms: int) -> None:
|
|
232
|
+
elapsed_days = (now_ms - p.last_update_ms) / (1000 * 60 * 60 * 24)
|
|
233
|
+
if elapsed_days <= 0:
|
|
234
|
+
return
|
|
235
|
+
decay = math.exp(-math.log(2) * elapsed_days / self.half_life_days)
|
|
236
|
+
# Decay only the "above-prior" mass; preserve the Laplace prior.
|
|
237
|
+
p.alpha = 1.0 + (p.alpha - 1.0) * decay
|
|
238
|
+
p.beta = 1.0 + (p.beta - 1.0) * decay
|
|
239
|
+
p.last_update_ms = now_ms
|
|
240
|
+
|
|
241
|
+
def record_outcome(
|
|
242
|
+
self,
|
|
243
|
+
agent_id: bytes,
|
|
244
|
+
topic: str,
|
|
245
|
+
*,
|
|
246
|
+
correct: bool,
|
|
247
|
+
weight: float = 1.0,
|
|
248
|
+
evidence_hlc_ms: int | None = None,
|
|
249
|
+
) -> ReputationSnapshot:
|
|
250
|
+
"""Update reputation given a confirmed outcome.
|
|
251
|
+
|
|
252
|
+
``evidence_hlc_ms`` is the HLC timestamp of the original claim (the
|
|
253
|
+
evidence age). When provided, the effective weight is multiplied by
|
|
254
|
+
``_staleness_factor`` so stale evidence has less influence than fresh
|
|
255
|
+
evidence. When absent, the current time is used (full weight).
|
|
256
|
+
"""
|
|
257
|
+
now_ms = int(time.time() * 1000)
|
|
258
|
+
if evidence_hlc_ms is not None:
|
|
259
|
+
stale_w = self._staleness_factor(
|
|
260
|
+
evidence_hlc_ms,
|
|
261
|
+
now_ms,
|
|
262
|
+
staleness_window_ms=self.staleness_window_ms,
|
|
263
|
+
lambda_staleness=self.lambda_staleness,
|
|
264
|
+
)
|
|
265
|
+
weight = weight * stale_w
|
|
266
|
+
p = self._get(agent_id, topic)
|
|
267
|
+
self._decay(p, now_ms=now_ms)
|
|
268
|
+
if correct:
|
|
269
|
+
p.alpha += weight
|
|
270
|
+
else:
|
|
271
|
+
# Smooth slashing: shave a fraction of α, increment β.
|
|
272
|
+
p.alpha = max(1.0, p.alpha * (1 - self.lam))
|
|
273
|
+
p.beta += self.mu * weight + weight # weighted bad outcome
|
|
274
|
+
# Push to score_history HERE — once per real observation.
|
|
275
|
+
# snapshot() must remain a pure read so consult/discover_mentors
|
|
276
|
+
# don't bias momentum_z by calling it n·log(n) times in panel
|
|
277
|
+
# selection (closes self-heal Cycle-3 Finding #16).
|
|
278
|
+
score = p.alpha / (p.alpha + p.beta)
|
|
279
|
+
p.score_history.append(score)
|
|
280
|
+
# P2-16: broadcast the applied delta to peers (fire-and-forget).
|
|
281
|
+
alpha_delta = weight if correct else 0.0
|
|
282
|
+
beta_delta = 0.0 if correct else self.mu * weight + weight
|
|
283
|
+
self._schedule_knows_delta_broadcast(agent_id, topic, alpha_delta, beta_delta)
|
|
284
|
+
return self.snapshot(agent_id, topic)
|
|
285
|
+
|
|
286
|
+
def vouch(self, voucher: bytes, vouchee: bytes, topic: str, *, strength: float = 1.0) -> None:
|
|
287
|
+
"""Boost vouchee's reputation by a fraction of voucher's confidence.
|
|
288
|
+
|
|
289
|
+
Cap (closes self-heal Cycle-1 Finding #4): a single vouch may
|
|
290
|
+
contribute at most 5% of the VOUCHER's own α — so a high-rep
|
|
291
|
+
agent cannot transfer their full reputation to a brand-new
|
|
292
|
+
cohort in one move (ring-laundering defence). The pre-cap line
|
|
293
|
+
`min(α, α * 1.05)` was a no-op (always returns α since 1.05>1).
|
|
294
|
+
"""
|
|
295
|
+
voucher_p = self._get(voucher, topic)
|
|
296
|
+
self._decay(voucher_p, now_ms=int(time.time() * 1000))
|
|
297
|
+
boost = strength * (self.snapshot(voucher, topic).score - self.threshold)
|
|
298
|
+
if boost <= 0:
|
|
299
|
+
return
|
|
300
|
+
# Hard cap: 5% of the voucher's above-prior mass.
|
|
301
|
+
cap = 0.05 * max(0.0, voucher_p.alpha - 1.0)
|
|
302
|
+
boost = min(boost, cap) if cap > 0 else 0.0
|
|
303
|
+
if boost <= 0:
|
|
304
|
+
return
|
|
305
|
+
p = self._get(vouchee, topic)
|
|
306
|
+
self._decay(p, now_ms=int(time.time() * 1000))
|
|
307
|
+
p.alpha += boost
|
|
308
|
+
|
|
309
|
+
def snapshot(self, agent_id: bytes, topic: str) -> ReputationSnapshot:
|
|
310
|
+
now_ms = int(time.time() * 1000)
|
|
311
|
+
p = self._get(agent_id, topic)
|
|
312
|
+
# Compute decayed values on a scratch copy — do NOT mutate p so
|
|
313
|
+
# concurrent reads don't race on last_update_ms (closes #53).
|
|
314
|
+
elapsed_days = (now_ms - p.last_update_ms) / (1000 * 60 * 60 * 24)
|
|
315
|
+
if elapsed_days > 0:
|
|
316
|
+
decay = math.exp(-math.log(2) * elapsed_days / self.half_life_days)
|
|
317
|
+
alpha = 1.0 + (p.alpha - 1.0) * decay
|
|
318
|
+
beta = 1.0 + (p.beta - 1.0) * decay
|
|
319
|
+
else:
|
|
320
|
+
alpha, beta = p.alpha, p.beta
|
|
321
|
+
score = alpha / (alpha + beta)
|
|
322
|
+
# Note: score_history is appended in record_outcome (the actual
|
|
323
|
+
# observation point), NOT here. snapshot() is a pure read for
|
|
324
|
+
# panel selection / discover_mentors / consult routing — appending
|
|
325
|
+
# on every read would n·log(n)-bias momentum_z.
|
|
326
|
+
z = 0.0
|
|
327
|
+
if len(p.score_history) >= 5:
|
|
328
|
+
mean = sum(p.score_history) / len(p.score_history)
|
|
329
|
+
var = sum((s - mean) ** 2 for s in p.score_history) / len(p.score_history)
|
|
330
|
+
std = math.sqrt(var) or 1e-6
|
|
331
|
+
z = (score - mean) / std
|
|
332
|
+
return ReputationSnapshot(
|
|
333
|
+
agent_id=bytes(agent_id),
|
|
334
|
+
topic=topic,
|
|
335
|
+
score=score,
|
|
336
|
+
n_observations=int(alpha + beta - 2),
|
|
337
|
+
last_updated_ms=p.last_update_ms,
|
|
338
|
+
momentum_z=z,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def above_threshold(self, agent_id: bytes, topic: str) -> bool:
|
|
342
|
+
return self.snapshot(agent_id, topic).score >= self.threshold
|
|
343
|
+
|
|
344
|
+
def update_with_correlated_agreement(
|
|
345
|
+
self,
|
|
346
|
+
reports: list,
|
|
347
|
+
*,
|
|
348
|
+
topic: str,
|
|
349
|
+
alpha: float = 1.0,
|
|
350
|
+
threshold: float = 0.0,
|
|
351
|
+
) -> dict[bytes, float]:
|
|
352
|
+
"""Apply Bayesian-Truth-Serum / correlated-agreement scoring.
|
|
353
|
+
|
|
354
|
+
See :mod:`hypermind.knowledge.correlated_agreement` (ROADMAP §0.2.2).
|
|
355
|
+
Returns the raw score map (reporter_id → score) so callers can
|
|
356
|
+
inspect calibration without a second computation.
|
|
357
|
+
"""
|
|
358
|
+
from hypermind.knowledge.correlated_agreement import (
|
|
359
|
+
update_kernel_with_reports,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return update_kernel_with_reports(
|
|
363
|
+
self, reports, topic=topic, alpha=alpha, threshold=threshold
|
|
364
|
+
)
|