GitHub Actions commited on
Commit
4aaae80
Β·
1 Parent(s): 6db565a

fix: 0 test failures; FileService; real RagService; emergency probe; chat return

Browse files

TESTS:
- conftest.py: nest_asyncio.apply() fixes Python 3.13 + pytest-asyncio 0.26
event-loop isolation β€” was causing 32 cross-test failures; now 102 passed 0 failed
- pyproject.toml: asyncio_mode=strict + asyncio_default_fixture_loop_scope=function

SERVICES (no mocks, no fakes in production):
- hearthnet/services/__init__.py: import real RagService not demo.RagService
- hearthnet/services/files/: new FileService providing file.put/file.get/file.list/
file.delete via bus (BLAKE3 CID, in-memory or disk store)
- node.py: register FileService in both install_demo_services and install_services
- marketplace: add market.delete capability (alias for expire)

UI BUGS FIXED:
- ui/tabs/chat.py: add missing return in except block (was returning None on error)
- ui/tabs/emergency.py: wire Run Connectivity Probe button with sync probe +
show results; was defined but never connected
- ui/tabs/settings.py: gen_invite gracefully handles missing nacl/community manifest

APP.PY (HF Space):
- Register FileService properly
- Richer seed corpus: 10 documents (water, power, CPR, first aid, shelter, food,
mesh routing, node setup x2, emergency comms plan)
- pyproject.toml: add PyNaCl, blake3, qrcode[svg] to dependencies

pyproject.toml: add nest-asyncio to dev deps; PyNaCl/blake3/qrcode to main deps

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. QUALITY_REPORT.md +260 -0
  2. app.py +88 -31
  3. docs/sample.txt +190 -0
  4. docs/screenshots/node-a-ask-tab.png +0 -0
  5. docs/screenshots/node-b-settings-tab.png +0 -0
  6. hearthnet/blobs/chunker.py +0 -1
  7. hearthnet/blobs/transfer.py +3 -1
  8. hearthnet/bus/router.py +2 -2
  9. hearthnet/bus/schema.py +3 -6
  10. hearthnet/bus/trace.py +1 -0
  11. hearthnet/civdef/__init__.py +1 -0
  12. hearthnet/civdef/service.py +25 -19
  13. hearthnet/cli.py +7 -2
  14. hearthnet/config.py +37 -16
  15. hearthnet/constants.py +5 -9
  16. hearthnet/crypto/envelope.py +4 -7
  17. hearthnet/crypto/kem.py +2 -3
  18. hearthnet/crypto/prekeys.py +1 -0
  19. hearthnet/crypto/ratchet.py +1 -0
  20. hearthnet/dht/bootstrap.py +2 -2
  21. hearthnet/dht/kademlia.py +9 -10
  22. hearthnet/discovery/__init__.py +7 -3
  23. hearthnet/discovery/mdns.py +4 -0
  24. hearthnet/discovery/peers.py +13 -5
  25. hearthnet/discovery/udp.py +11 -7
  26. hearthnet/distributed_inference/__init__.py +3 -2
  27. hearthnet/distributed_inference/pipeline.py +4 -1
  28. hearthnet/distributed_inference/shard.py +6 -6
  29. hearthnet/emergency/detector.py +7 -6
  30. hearthnet/emergency/state.py +2 -1
  31. hearthnet/events/__init__.py +7 -7
  32. hearthnet/events/log.py +9 -5
  33. hearthnet/events/snapshot.py +10 -6
  34. hearthnet/events/sync.py +1 -3
  35. hearthnet/events/types.py +5 -5
  36. hearthnet/evidence/__init__.py +3 -2
  37. hearthnet/evidence/store.py +12 -7
  38. hearthnet/federation/manifest.py +17 -16
  39. hearthnet/federation/peering.py +1 -0
  40. hearthnet/federation/service.py +16 -12
  41. hearthnet/fedlearn/__init__.py +3 -2
  42. hearthnet/fedlearn/coordinator.py +3 -1
  43. hearthnet/identity/keys.py +8 -13
  44. hearthnet/identity/manifest.py +26 -12
  45. hearthnet/identity/tokens.py +9 -8
  46. hearthnet/lora/__init__.py +7 -1
  47. hearthnet/lora/service.py +6 -3
  48. hearthnet/mobile/invite.py +3 -4
  49. hearthnet/mobile/push_authority.py +7 -3
  50. hearthnet/moe/__init__.py +1 -0
QUALITY_REPORT.md ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HearthNet Quality Assurance Report
2
+ **Date:** June 10, 2026
3
+ **Status:** βœ… Quality Check Scripts Created and Executed
4
+
5
+ ---
6
+
7
+ ## Executive Summary
8
+
9
+ Two new management scripts have been created for the HearthNet project:
10
+ 1. **check_quality.py** – Automated quality checking script
11
+ 2. **app_manager.bat** – Windows batch menu for app management
12
+
13
+ Quality checks were executed on the codebase with the following results:
14
+
15
+ ---
16
+
17
+ ## Quality Check Results
18
+
19
+ ### βœ… Ruff Linter & Formatter
20
+ - **Status:** 167 issues **fixed**, 92 **remaining**
21
+ - **Fixed Issues:**
22
+ - Unused imports removed
23
+ - Import organization improved
24
+ - Code simplifications applied (SIM patterns)
25
+ - Trailing whitespace removed
26
+ - Module-level import positioning corrected
27
+
28
+ - **Remaining Issues (Manual Review Needed):**
29
+ - 52 unsafe fixes available (use `--unsafe-fixes` if approved)
30
+ - Code simplification suggestions (try/except patterns, nested conditionals)
31
+ - Variable redefinitions in constants.py
32
+ - Module shadowing (hearthnet/types.py)
33
+
34
+ **Action:** Most auto-fixable issues resolved. Remaining issues require thoughtful review.
35
+
36
+ ---
37
+
38
+ ### πŸ” Bandit Security Check
39
+ - **Status:** βœ… **No critical or high-severity vulnerabilities**
40
+ - **Findings Summary:**
41
+ - **Low Severity (58 issues):**
42
+ - Try/except/pass patterns (non-fatal exception handling)
43
+ - Use of assert statements in code (assert shouldn't be used for runtime checks)
44
+ - Some subprocess calls need explicit `check=True` parameter
45
+
46
+ - **Medium Severity (11 issues):**
47
+ - Binding to `0.0.0.0` (4 instances) – intentional for peer mesh
48
+ - URL opening with urllib (2 instances) – used for probe/config only
49
+ - Hugging Face unsafe downloads (5 instances) – requires revision pinning
50
+ - Hardcoded SQL expression (1 instance) – marked with nosec, uses computed placeholders
51
+
52
+ **Recommendation:** Most medium-severity findings are intentional for P2P functionality. Hugging Face downloads should use revision pinning for production.
53
+
54
+ ---
55
+
56
+ ### πŸ“ MyPy Type Checking
57
+ - **Status:** ⚠️ **27 type errors found**
58
+ - **Main Issues:**
59
+ - Variable redefinitions in constants.py (embed/rerank)
60
+ - Unused type ignore comments (8 instances)
61
+ - Type incompatibilities in capability descriptors (str vs tuple[int,int])
62
+ - Assignment type mismatches (set vs list in chat service)
63
+ - Missing attribute errors in transport layer
64
+
65
+ **Action:** Type errors are moderate. Most can be fixed by:
66
+ 1. Removing unused `type: ignore` comments
67
+ 2. Fixing constant duplicates
68
+ 3. Updating capability descriptor type annotations
69
+
70
+ ---
71
+
72
+ ## Scripts Created
73
+
74
+ ### 1. `scripts/check_quality.py`
75
+ **Purpose:** Run quality checks with proper error handling
76
+ **Checks Executed:**
77
+ - Ruff format checking
78
+ - Ruff linting
79
+ - Bandit security analysis
80
+ - MyPy type checking
81
+
82
+ **Features:**
83
+ - Timeout protection (no hanging processes)
84
+ - Clear pass/fail summary
85
+ - Helpful tips for fixing issues
86
+ - Only critical checks (avoids pip audit delays)
87
+
88
+ **Usage:**
89
+ ```bash
90
+ python scripts/check_quality.py
91
+ ```
92
+
93
+ **Time:** ~4-5 minutes for complete run
94
+
95
+ ---
96
+
97
+ ### 2. `scripts/app_manager.bat`
98
+ **Purpose:** Windows batch menu for application management
99
+ **Features:**
100
+ - πŸš€ Start HearthNet (CLI or Gradio UI)
101
+ - πŸ›‘ Stop running instances
102
+ - πŸ“¦ Install dependencies
103
+ - βš™οΈ Configuration management
104
+ - πŸ” Quality checks integration
105
+ - πŸ§ͺ Test runner
106
+ - πŸ“š Documentation access
107
+
108
+ **Menu Options:**
109
+ ```
110
+ 1. Start HearthNet (CLI)
111
+ 2. Start HearthNet (Gradio Web UI)
112
+ 3. Start Multi-Node Demo
113
+ 4. Stop HearthNet
114
+ 5. Install Dependencies
115
+ 6. Install Dev Dependencies
116
+ 7. Configure Settings
117
+ 8. Run Quality Checks
118
+ 9. Run Tests
119
+ A. Generate Screenshots
120
+ B. Open Logs
121
+ C. Open Documentation
122
+ 0. Exit
123
+ ```
124
+
125
+ **Usage:**
126
+ ```batch
127
+ scripts\app_manager.bat
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Issues Found & Status
133
+
134
+ ### πŸ”΄ Critical Issues: 0
135
+
136
+ ### 🟠 Medium Issues: ~15
137
+ 1. **Constant Duplicates** (constants.py)
138
+ - EMBED_MAX_TEXTS, EMBED_MAX_CHARS, RERANK_MAX_DOCS redefined
139
+ - **Fix:** Remove duplicate definitions
140
+
141
+ 2. **HuggingFace Model Downloads**
142
+ - Missing revision pinning in 5 locations
143
+ - **Fix:** Add `revision` parameter to all `from_pretrained()` calls
144
+
145
+ 3. **Type Mismatches**
146
+ - Capability descriptor version expects tuple[int,int], gets str
147
+ - **Fix:** Convert string versions to tuples
148
+
149
+ ### 🟑 Low Issues: ~100+
150
+ - Unused type ignore comments (can remove)
151
+ - Try/except/pass patterns (intentional, documented)
152
+ - Variable names (minor style issues)
153
+
154
+ ---
155
+
156
+ ## Next Steps - Recommended Priority
157
+
158
+ ### Priority 1 (Complete before merge)
159
+ - [ ] Remove duplicate constants
160
+ - [ ] Fix type incompatibilities in capability descriptors
161
+ - [ ] Remove unused `type: ignore` comments from mypy
162
+
163
+ ### Priority 2 (Before production deployment)
164
+ - [ ] Add revision pinning to all HuggingFace downloads
165
+ - [ ] Review and address try/except patterns if stricter error handling needed
166
+ - [ ] Update assert statements to proper runtime checks
167
+
168
+ ### Priority 3 (Nice to have)
169
+ - [ ] Apply unsafe ruff fixes (after review)
170
+ - [ ] Simplify nested conditionals per SIM patterns
171
+ - [ ] Consider linter configuration updates
172
+
173
+ ---
174
+
175
+ ## Quality Metrics
176
+
177
+ | Metric | Value | Status |
178
+ |--------|-------|--------|
179
+ | Lines of Code | 15,106 | βœ… |
180
+ | Ruff Issues Fixed | 167 | βœ… |
181
+ | Ruff Issues Remaining | 92 | ⚠️ |
182
+ | Security Issues (High) | 0 | βœ… |
183
+ | Security Issues (Medium) | 11 | ⚠️ |
184
+ | Type Errors | 27 | ⚠️ |
185
+ | Tests Passing | TBD | πŸ”„ |
186
+
187
+ ---
188
+
189
+ ## Scripts Location
190
+
191
+ ```
192
+ scripts/
193
+ β”œβ”€β”€ check_quality.py ← Quality check automation
194
+ β”œβ”€β”€ app_manager.bat ← Windows app management menu
195
+ β”œβ”€β”€ demo_two_nodes.py ← Multi-node demo
196
+ └── gen_screenshots.py ← UI screenshot generator
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Usage Examples
202
+
203
+ ### Quick Quality Check
204
+ ```bash
205
+ cd c:\Users\Chris4K\Projekte\HearthNet
206
+ python scripts\check_quality.py
207
+ ```
208
+
209
+ ### Run via Batch Menu
210
+ ```bash
211
+ scripts\app_manager.bat
212
+ # Then press 8 for Quality Checks
213
+ ```
214
+
215
+ ### Fix Ruff Issues
216
+ ```bash
217
+ ruff check hearthnet app.py --fix
218
+ ruff format hearthnet app.py
219
+ ```
220
+
221
+ ### Check Specific Issues
222
+ ```bash
223
+ bandit -r hearthnet
224
+ mypy hearthnet --ignore-missing-imports
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Configuration Notes
230
+
231
+ ### Ruff Configuration
232
+ - **Target:** Python 3.12
233
+ - **Line Length:** 100
234
+ - **Excluded:** .git, .venv, .pytest_cache, etc.
235
+ - **Format:** Double quotes, LF line endings
236
+
237
+ ### Bandit Configuration
238
+ - **Excluded:** tests, .git, .venv
239
+ - **Skipped:** B101 (assert_used in tests is OK)
240
+
241
+ ### MyPy Configuration
242
+ - **Python Version:** 3.12
243
+ - **Option:** `--ignore-missing-imports` (many optional deps)
244
+
245
+ ---
246
+
247
+ ## Conclusion
248
+
249
+ βœ… **Quality infrastructure is now in place** with:
250
+ - Automated quality checking
251
+ - User-friendly Windows batch menu
252
+ - Clear issue reporting
253
+ - Actionable recommendations
254
+
255
+ The codebase has **no critical issues** and is ready for continued development. Priority should be given to fixing the medium-level type issues before major refactoring or feature additions.
256
+
257
+ ---
258
+
259
+ *Generated by HearthNet Quality Check System*
260
+ *Last Run: June 10, 2026 at 20:15 UTC*
app.py CHANGED
@@ -26,17 +26,17 @@ Quick start (local, full features):
26
 
27
  See docs/HOWTO.md for Raspberry Pi, Docker, and multi-node mesh setup.
28
  """
 
29
  from __future__ import annotations
30
 
31
  import os
32
 
33
- import gradio as gr
34
-
35
  # ─────────────────────────────────────────────────────────────────────────────
36
  # Optional HF Spaces GPU decorator
37
  # ─────────────────────────────────────────────────────────────────────────────
38
  try:
39
  import spaces as _spaces # type: ignore[import]
 
40
  HF_SPACES = True
41
  except ImportError:
42
  HF_SPACES = False
@@ -55,16 +55,18 @@ SEED_CORPUS = [
55
  "text": (
56
  "If the mains supply is disrupted, use stored clean water first. "
57
  "Rainwater should be filtered through clean cloth, brought to a rolling "
58
- "boil for at least one minute, and stored in a clean covered container."
 
59
  ),
60
  },
61
  {
62
  "id": "power.001",
63
  "title": "Power Outage",
64
  "text": (
65
- "Keep refrigerators closed. Disconnect sensitive devices. Reserve battery "
66
- "banks for communication. Share verified charging points through the local "
67
- "marketplace."
 
68
  ),
69
  },
70
  {
@@ -73,26 +75,83 @@ SEED_CORPUS = [
73
  "text": (
74
  "A HearthNet UI sends requests to a capability bus. The bus scores local "
75
  "capabilities higher than remote ones and routes to the best available "
76
- "provider. If a node is quarantined the bus fails over automatically."
 
77
  ),
78
  },
79
  {
80
  "id": "firstaid.001",
81
- "title": "First Aid Basics",
82
  "text": (
83
- "Check scene safety first. Call local emergency contacts when available. "
84
- "Assess breathing. Control severe bleeding with direct pressure. Keep the "
85
- "person warm until help arrives."
 
 
 
 
 
 
 
 
 
 
 
 
86
  ),
87
  },
88
  {
89
  "id": "setup.001",
90
- "title": "Node Setup",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  "text": (
92
- "Install HearthNet with pip install hearthnet. Run python -m hearthnet.cli run "
93
- "to start a node. Other devices on the same LAN discover it automatically via "
94
- "mDNS. Use the Settings > Join This Mesh section to generate an invite QR code "
95
- "for devices on different networks."
 
 
 
 
 
 
 
 
 
 
 
96
  ),
97
  },
98
  ]
@@ -105,11 +164,12 @@ def _build_node():
105
  Falls back to _UnavailableBackend if transformers is not installed.
106
  """
107
  from hearthnet.node import HearthNode
108
- from hearthnet.services.llm.service import LlmService
109
- from hearthnet.services.llm.backends.hf_local import HfLocalBackend
110
- from hearthnet.services.marketplace.service import MarketplaceService
111
  from hearthnet.services.chat.service import ChatService
112
  from hearthnet.services.demo import RagService as DemoRagService
 
 
 
 
113
 
114
  node = HearthNode(
115
  node_id="hf-space",
@@ -125,10 +185,13 @@ def _build_node():
125
  if HF_SPACES:
126
  import asyncio
127
  import time as _time
 
128
  from hearthnet.services.llm.backends.base import ChatResult
129
 
130
  @_spaces.GPU(duration=120)
131
- def _gpu_pipeline_call(pipeline, prompt: str, max_new_tokens: int, temperature: float) -> list:
 
 
132
  """GPU-wrapped pipeline call. ZeroGPU allocates GPU for this function."""
133
  return pipeline(
134
  prompt,
@@ -158,13 +221,14 @@ def _build_node():
158
  raise RuntimeError("HF model not loaded")
159
  t0 = _time.monotonic()
160
  prompt = (
161
- "\n".join(f"{m['role']}: {m['content']}" for m in messages)
162
- + "\nassistant:"
163
  )
164
  loop = asyncio.get_event_loop()
165
  result = await loop.run_in_executor(
166
  None,
167
- lambda: self._gpu_pipeline_call(self._pipeline, prompt, max_tokens, temperature),
 
 
168
  )
169
  text = result[0]["generated_text"] if result else ""
170
  ms = int((_time.monotonic() - t0) * 1000)
@@ -185,7 +249,6 @@ def _build_node():
185
  node.bus.register_service(llm)
186
 
187
  # RAG β€” pre-seeded community corpus using demo RagService (in-memory)
188
- from hearthnet.services.demo import RagService as DemoRagService
189
  rag = DemoRagService(corpus="community")
190
  rag.documents = list(SEED_CORPUS)
191
  node.bus.register_service(rag)
@@ -193,13 +256,7 @@ def _build_node():
193
  # Marketplace, Chat, Files
194
  node.bus.register_service(MarketplaceService())
195
  node.bus.register_service(ChatService(node.node_id))
196
-
197
- # File blobs (in-memory for Space; persisted to disk on local install)
198
- try:
199
- from hearthnet.services.files.service import FileService
200
- node.bus.register_service(FileService())
201
- except Exception:
202
- pass
203
 
204
  return node
205
 
 
26
 
27
  See docs/HOWTO.md for Raspberry Pi, Docker, and multi-node mesh setup.
28
  """
29
+
30
  from __future__ import annotations
31
 
32
  import os
33
 
 
 
34
  # ─────────────────────────────────────────────────────────────────────────────
35
  # Optional HF Spaces GPU decorator
36
  # ─────────────────────────────────────────────────────────────────────────────
37
  try:
38
  import spaces as _spaces # type: ignore[import]
39
+
40
  HF_SPACES = True
41
  except ImportError:
42
  HF_SPACES = False
 
55
  "text": (
56
  "If the mains supply is disrupted, use stored clean water first. "
57
  "Rainwater should be filtered through clean cloth, brought to a rolling "
58
+ "boil for at least one minute, and stored in a clean covered container. "
59
+ "Adult daily minimum: 3 litres for drinking and sanitation."
60
  ),
61
  },
62
  {
63
  "id": "power.001",
64
  "title": "Power Outage",
65
  "text": (
66
+ "Keep refrigerators closed to preserve food up to 4 hours. "
67
+ "Disconnect sensitive electronics. Reserve battery banks for communication. "
68
+ "Share verified charging points through the local marketplace. "
69
+ "Candles are a fire risk β€” use battery or wind-up torches."
70
  ),
71
  },
72
  {
 
75
  "text": (
76
  "A HearthNet UI sends requests to a capability bus. The bus scores local "
77
  "capabilities higher than remote ones and routes to the best available "
78
+ "provider. If a node is quarantined the bus fails over automatically. "
79
+ "RAG corpus routing uses the 'corpus' parameter to match the right node."
80
  ),
81
  },
82
  {
83
  "id": "firstaid.001",
84
+ "title": "First Aid β€” Bleeding",
85
  "text": (
86
+ "Apply direct firm pressure to the wound with a clean cloth. "
87
+ "Maintain pressure for at least 10 minutes. Do not remove the cloth β€” "
88
+ "add more on top if it soaks through. Elevate the limb above heart level "
89
+ "if possible. Seek emergency care if bleeding is severe or arterial."
90
+ ),
91
+ },
92
+ {
93
+ "id": "firstaid.002",
94
+ "title": "CPR Basics",
95
+ "text": (
96
+ "If a person is unresponsive and not breathing normally: call emergency services, "
97
+ "then give 30 chest compressions (hard, fast, centre of chest) followed by "
98
+ "2 rescue breaths. Continue the 30:2 cycle until help arrives or the person "
99
+ "recovers. Hands-only CPR (compressions without rescue breaths) is acceptable "
100
+ "for untrained bystanders."
101
  ),
102
  },
103
  {
104
  "id": "setup.001",
105
+ "title": "Node Setup β€” Quick Start",
106
+ "text": (
107
+ "Install HearthNet with: pip install hearthnet. "
108
+ "Run: python -m hearthnet.cli run "
109
+ "to start a node. Open http://localhost:7860 in your browser. "
110
+ "Other devices on the same LAN discover your node automatically via mDNS. "
111
+ "Use the Settings tab to generate an invite QR for devices on other networks."
112
+ ),
113
+ },
114
+ {
115
+ "id": "setup.002",
116
+ "title": "Node Setup β€” Specialized Nodes",
117
+ "text": (
118
+ "Register only the capabilities your hardware supports. "
119
+ "An OCR Raspberry Pi: register OcrService. "
120
+ "A medical knowledge node: register RagService with a medical corpus. "
121
+ "A thin client (phone): register no services β€” all bus calls route to peers. "
122
+ "The bus auto-discovers and routes to the best provider in the mesh."
123
+ ),
124
+ },
125
+ {
126
+ "id": "emergency.001",
127
+ "title": "Emergency Communication Plan",
128
+ "text": (
129
+ "Before a disaster: exchange node IDs with neighbours. "
130
+ "During internet outage: HearthNet switches to offline mode automatically. "
131
+ "All routing stays local. Use the mesh to share offers and requests. "
132
+ "For emergency alerts, post to the Marketplace with category=emergency. "
133
+ "Battery-powered device with HearthNet can serve the whole neighbourhood."
134
+ ),
135
+ },
136
+ {
137
+ "id": "food.001",
138
+ "title": "Emergency Food Safety",
139
  "text": (
140
+ "In a power outage, refrigerated food is safe for up to 4 hours. "
141
+ "Frozen food stays safe for 24-48 hours if the freezer stays closed. "
142
+ "Discard meat, poultry, seafood, dairy, or cooked food left above 4Β°C "
143
+ "for more than 2 hours. When in doubt, throw it out."
144
+ ),
145
+ },
146
+ {
147
+ "id": "shelter.001",
148
+ "title": "Shelter in Place",
149
+ "text": (
150
+ "During chemical or biological hazards, stay indoors. "
151
+ "Close all windows and doors. Turn off HVAC. "
152
+ "Seal gaps with wet towels or tape. "
153
+ "Monitor emergency broadcasts on battery radio. "
154
+ "Do not leave until authorities give the all-clear."
155
  ),
156
  },
157
  ]
 
164
  Falls back to _UnavailableBackend if transformers is not installed.
165
  """
166
  from hearthnet.node import HearthNode
 
 
 
167
  from hearthnet.services.chat.service import ChatService
168
  from hearthnet.services.demo import RagService as DemoRagService
169
+ from hearthnet.services.files.service import FileService
170
+ from hearthnet.services.llm.backends.hf_local import HfLocalBackend
171
+ from hearthnet.services.llm.service import LlmService
172
+ from hearthnet.services.marketplace.service import MarketplaceService
173
 
174
  node = HearthNode(
175
  node_id="hf-space",
 
185
  if HF_SPACES:
186
  import asyncio
187
  import time as _time
188
+
189
  from hearthnet.services.llm.backends.base import ChatResult
190
 
191
  @_spaces.GPU(duration=120)
192
+ def _gpu_pipeline_call(
193
+ pipeline, prompt: str, max_new_tokens: int, temperature: float
194
+ ) -> list:
195
  """GPU-wrapped pipeline call. ZeroGPU allocates GPU for this function."""
196
  return pipeline(
197
  prompt,
 
221
  raise RuntimeError("HF model not loaded")
222
  t0 = _time.monotonic()
223
  prompt = (
224
+ "\n".join(f"{m['role']}: {m['content']}" for m in messages) + "\nassistant:"
 
225
  )
226
  loop = asyncio.get_event_loop()
227
  result = await loop.run_in_executor(
228
  None,
229
+ lambda: self._gpu_pipeline_call(
230
+ self._pipeline, prompt, max_tokens, temperature
231
+ ),
232
  )
233
  text = result[0]["generated_text"] if result else ""
234
  ms = int((_time.monotonic() - t0) * 1000)
 
249
  node.bus.register_service(llm)
250
 
251
  # RAG β€” pre-seeded community corpus using demo RagService (in-memory)
 
252
  rag = DemoRagService(corpus="community")
253
  rag.documents = list(SEED_CORPUS)
254
  node.bus.register_service(rag)
 
256
  # Marketplace, Chat, Files
257
  node.bus.register_service(MarketplaceService())
258
  node.bus.register_service(ChatService(node.node_id))
259
+ node.bus.register_service(FileService())
 
 
 
 
 
 
260
 
261
  return node
262
 
docs/sample.txt ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Geschichte
2
+ Die VorlΓ€ufer des modernen Computers
3
+ Die moderne Computertechnologie, wie wir sie heute kennen, entwickelte sich im Vergleich zu anderen ElektrogerÀten der Neuzeit sehr schnell. Die Geschichte der Entwicklung des Computers an sich jedoch reicht zurück bis in die Antike und umfasst deutlich mehr, als nur die modernen Computertechnologien oder mechanischen bzw. elektrischen Hilfsmittel (Rechenmaschinen oder Hardware). Sie umfasst z. B. auch die Entwicklung von Zahlensystemen und Rechenmethoden, die etwa für einfache SchreibgerÀte auf Papier und Tafeln entwickelt wurden. Im Folgenden wird versucht, einen Überblick über diese Entwicklungen zu geben.
4
+
5
+ Zahlen und Ziffern als Grundlage der Computergeschichte
6
+ Das Konzept der Zahlen lΓ€sst sich auf keine konkreten Wurzeln zurΓΌckfΓΌhren und hat sich wahrscheinlich mit den ersten Notwendigkeiten der Kommunikation zwischen zwei Individuen entwickelt. Man findet in allen bekannten Sprachen mindestens fΓΌr die Zahlen eins und zwei Entsprechungen.
7
+
8
+ Als Weiterentwicklung ist der Übergang von der reinen Anzahlbenennung zum Gebrauch mathematischer Rechenoperationen wie Addition, Subtraktion, Multiplikation und Division anzusehen; auch Quadratzahlen und Quadratwurzel sind hierunter zu fassen. Diese Operationen wurden formalisiert (in Formeln dargestellt) und dadurch überprüfbar. Daraus entwickelten sich dann weiterführende Betrachtungen, etwa die von Euklid entwickelte Darstellung des grâßten gemeinsamen Teilers.
9
+
10
+ Im Mittelalter erreichte das ursprünglich aus Indien stammende arabische Zahlensystem Europa und erlaubte eine grâßere Systematisierung bei der Arbeit mit Zahlen. Es erlaubte die Darstellung von Zahlen, Ausdrücken und Formeln auf Papier und die Tabellierung von mathematischen Funktionen wie der Quadratwurzel, des einfachen Logarithmus und trigonometrischer Funktionen. Zur Zeit der Arbeiten von Isaac Newton war Papier und Velin eine bedeutende Ressource für Rechenaufgaben und ist dies bis in die heutige Zeit geblieben, in der Forscher wie Enrico Fermi seitenweise Papier mit mathematischen Berechnungen füllten und Richard Feynman jeden mathematischen Schritt mit der Hand bis zur Lâsung berechnete, obwohl es zu seiner Zeit bereits programmierbare Rechner gab.
11
+
12
+ FrΓΌhe Entwicklung von Rechenmaschinen und -hilfsmitteln
13
+
14
+ Der Abakus
15
+ Das frΓΌheste GerΓ€t, das in rudimentΓ€ren AnsΓ€tzen mit einem heutigen Computer verglichen werden kann, ist der Abakus, eine mechanische Rechenhilfe, die vermutlich um 1100 v. Chr. im indochinesischen Kulturraum erfunden wurde. Der Abakus wurde bis ins 17. Jahrhundert benutzt und dann durch die ersten Rechenmaschinen ersetzt. In einigen Regionen der Welt wird der Abakus auch heute noch immer verwendet. Einem Γ€hnlichen Zweck diente auch das Rechenbrett des Pythagoras.
16
+
17
+
18
+ Mechanismus von Antikythera
19
+ Bereits im 1. Jahrhundert v. Chr. wurde mit dem RΓ€derwerk von Antikythera die erste Rechenmaschine erfunden.[1] Das GerΓ€t diente vermutlich fΓΌr astronomische Berechnungen und funktionierte mit einem Differentialgetriebe.
20
+
21
+ Mit dem Untergang der Antike kam der technische Fortschritt in Mittel- und Westeuropa fast zum Stillstand und in den Zeiten der VΓΆlkerwanderung ging viel Wissen verloren (so beispielsweise auch das RΓ€derwerk von Antikythera, das erst 1902 wiederentdeckt wurde) oder wurde nur noch im ostrΓΆmischen Reichsteil bewahrt. Die muslimischen Eroberer der ostrΓΆmischen Provinzen und schließlich Ost-Roms (Konstantinopel) nutzten dieses Wissen und entwickelten es weiter. Durch die KreuzzΓΌge und spΓ€tere Handelskontakte zwischen Abend- und Morgenland sowie die muslimische Herrschaft auf der iberischen Halbinsel, sickerte antikes Wissen und die darauf aufbauenden arabischen Erkenntnisse langsam wieder nach West- und Mitteleuropa ein. Ab der Neuzeit begann sich der Motor des technischen Fortschritts wieder langsam zu drehen und beschleunigte fortan – und dies tut er bis heute.
22
+
23
+
24
+ Der Rechenschieber, eine der wichtigsten mechanischen Rechenhilfen fΓΌr die Multiplikation und Division
25
+ 1614 publizierte John Napier seine Logarithmentafel. Mitentdecker der Logarithmen ist Jost BΓΌrgi. 1623 baute Wilhelm Schickard die erste Vier-Spezies-Maschine mit getrennten Werken fΓΌr Addition/Subtraktion und Multiplikation/Division und damit den ersten mechanischen Rechner, wodurch er zum β€žVater der ComputerΓ€raβ€œ wurde. Seine Konstruktion basierte auf dem Zusammenspiel von ZahnrΓ€dern, die im Wesentlichen aus dem Bereich der Uhrmacherkunst stammten und dort genutzt wurden, wodurch seine Maschine den Namen β€žRechenuhrβ€œ erhielt. Ein weiteres Exemplar war fΓΌr Johannes Keplers astronomische Berechnungen bestimmt, verbrannte aber halbfertig. Schickards eigenes GerΓ€t ist verschollen.
26
+
27
+ 1642 folgte Blaise Pascal mit seiner Zweispezies-Rechenmaschine, der Pascaline. 1668 entwickelte Samuel Morland eine Rechenmaschine, die erstmals nicht dezimal addierte, sondern auf das englische Geldsystem abgestimmt war. 1673 baute Gottfried Wilhelm Leibniz seine erste Vierspezies-Maschine und erfand 1703 (erneut) das binΓ€re Zahlensystem (Dualsystem), das spΓ€ter die Grundlage fΓΌr die Digitalrechner und darauf aufbauend die digitale Revolution wurde.
28
+
29
+
30
+ Mechanischer Rechner von 1914
31
+ 1805 nutzte Joseph-Marie Jacquard Lochkarten, um WebstΓΌhle zu steuern. 1820 baute Charles Xavier Thomas de Colmar das β€žArithmometerβ€œ, den ersten Rechner, der in Massenproduktion hergestellt wurde und somit den Computer fΓΌr Großunternehmen erschwinglich machte. Charles Babbage entwickelte von 1820 bis 1822 die Differenzmaschine (englisch Difference Engine) und 1837 die Analytical Engine, konnte sie aber aus Geldmangel nicht bauen. 1843 bauten Edvard und George Scheutz in Stockholm den ersten mechanischen Computer nach den Ideen von Babbage. Im gleichen Jahr entwickelte Ada Lovelace eine Methode zur Programmierung von Computern nach dem Babbage-System und schrieb damit das erste Computerprogramm. 1890 wurde die US-VolkszΓ€hlung mit Hilfe des Lochkartensystems von Herman Hollerith durchgefΓΌhrt. 1912 baute Torres y Quevedo eine SchachΒ­maschine, die mit KΓΆnig und Turm einen KΓΆnig matt setzen konnte, und somit den ersten Spielcomputer.
32
+
33
+ Mechanische Rechner wie die darauf folgenden Addierer, der Comptometer, der Monroe-Kalkulator, die Curta und der Addo-X wurden bis in die 1970er Jahre genutzt. Diese Rechner nutzten alle das Dezimalsystem. Dies galt sowohl fΓΌr die Rechner von Charles Babbage um 1800 wie auch fΓΌr den ENIAC von 1945, den ersten vollelektronischen Universalrechner ΓΌberhaupt.
34
+
35
+ Es wurden jedoch auch nichtmechanische Rechner gebaut, wie der Wasserintegrator.
36
+
37
+ Von 1935 ΓΌber die Zuse Z1 bis zur Turing-Bombe
38
+ 1935 stellten IBM die IBM 601 vor, eine Lochkartenmaschine, die eine Multiplikation pro Sekunde durchfΓΌhren konnte. Es wurden ca. 1500 Exemplare verkauft. 1937 meldete Konrad Zuse zwei Patente an, die bereits alle Elemente der so genannten Von-Neumann-Architektur beschreiben. Im selben Jahr baute John Atanasoff zusammen mit dem Doktoranden Clifford Berry einen der ersten Digitalrechner, den Atanasoff-Berry-Computer, und Alan Turing publizierte einen Artikel, der die Turingmaschine, ein abstraktes Modell zur Definition des Algorithmusbegriffs, beschreibt.
39
+
40
+ 1938 stellte Konrad Zuse die Zuse Z1 fertig, einen frei programmierbaren mechanischen Rechner, der allerdings aufgrund von Problemen mit der FertigungsprΓ€zision nie voll funktionstΓΌchtig war. Die Z1 verfΓΌgte bereits ΓΌber Gleitkommarechnung. Sie wurde im Krieg zerstΓΆrt und spΓ€ter nach OriginalplΓ€nen neu gefertigt, die Teile wurden auf modernen FrΓ€s- und DrehbΓ€nken hergestellt. Dieser Nachbau der Z1, der im Deutschen Technikmuseum in Berlin steht, ist mechanisch voll funktionsfΓ€hig und hat eine Rechengeschwindigkeit von 1 Hz, vollzieht also eine Rechenoperation pro Sekunde. Ebenfalls 1938 publizierte Claude Shannon einen Artikel darΓΌber, wie man symbolische Logik mit Relais implementieren kann. (Lit.: Shannon 1938)
41
+
42
+ WΓ€hrend des Zweiten Weltkrieges gab Alan Turing die entscheidenden Hinweise zur Entzifferung der Enigma-Codes und baute dafΓΌr einen speziellen mechanischen Rechner, Turing-Bombe genannt.
43
+
44
+ Entwicklung des modernen turingmΓ€chtigen Computers
45
+ Bis zum Ende des Zweiten Weltkrieges
46
+
47
+ Nachbau der Zuse Z3 im Deutschen Museum in MΓΌnchen
48
+ Ebenfalls im Krieg (1941) baute Konrad Zuse die erste funktionstΓΌchtige programmgesteuerte binΓ€re Rechenmaschine, bestehend aus einer großen Zahl von Relais, die Zuse Z3. Wie 1998 bewiesen werden konnte, war die Z3 turingmΓ€chtig und damit außerdem die erste Maschine, die – im Rahmen des verfΓΌgbaren Speicherplatzes – beliebige Algorithmen automatisch ausfΓΌhren konnte. Aufgrund dieser Eigenschaften wird sie oft als erster funktionsfΓ€higer Computer der Geschichte betrachtet.[2] Die nΓ€chsten Digitalrechner waren der in den USA gebaute Atanasoff-Berry-Computer (Inbetriebnahme 1941) und die britische Colossus (1941). Sie dienten speziellen Aufgaben und waren nicht turingmΓ€chtig. Auch Maschinen auf analoger Basis wurden entwickelt.
49
+
50
+
51
+ Colossus Mark II
52
+ Auf das Jahr 1943 wird auch die angeblich von IBM-Chef Thomas J. Watson stammende Aussage β€žIch glaube, es gibt einen weltweiten Bedarf an vielleicht fΓΌnf Computern.β€œ datiert. Im selben Jahr stellte Tommy Flowers mit seinem Team in Bletchley Park den ersten β€žColossusβ€œ fertig. 1944 erfolgte die Fertigstellung des ASCC (Automatic Sequence Controlled Computer, β€žMark Iβ€œ durch Howard H. Aiken) und das Team um Reinold Weber stellte eine EntschlΓΌsselungsmaschine fΓΌr das VerschlΓΌsselungsgerΓ€t M-209 der US-StreitkrΓ€fte fertig.[3] Zuse hatte schließlich bis MΓ€rz 1945 seine am 21. Dezember 1943 bei einem Bombenangriff zerstΓΆrte Z3 durch die deutlich verbesserte Zuse Z4 ersetzt, den damals einzigen turingmΓ€chtigen Computer in Europa, der von 1950 bis 1955 als zentraler Rechner der ETH ZΓΌrich genutzt wurde.
53
+
54
+
55
+ Computermodell Land Inbetriebnahme Gleitkomma-
56
+ arithmetik BinΓ€r Elektronisch Programmierbar TuringmΓ€chtig
57
+ Zuse Z3 Deutschland Mai 1941 Ja Ja Nein Ja, mittels Lochstreifen ΓΌber Umwege, nie genutzt
58
+ Atanasoff-Berry-Computer USA Sommer 1941 Nein Ja Ja Nein Nein
59
+ Colossus UK 1943 Nein Ja Ja Teilweise, durch NeuΒ­verΒ­kabelung Nein
60
+ Mark I USA 1944 Nein Nein Nein Ja, mittels Lochstreifen Ja
61
+ Zuse Z4 Deutschland MΓ€rz 1945 Ja Ja Nein Ja, mittels Lochstreifen keine bedingte Sprunganweisung
62
+ um 1950 Ja Ja Nein Ja, mittels Lochstreifen Ja
63
+ ENIAC USA 1946 Nein Nein Ja Teilweise, durch NeuΒ­verΒ­kabelung Ja
64
+ 1948 Nein Nein Ja Ja, mittels WiderΒ­standsΒ­matrix Ja
65
+ Nachkriegszeit
66
+
67
+ ENIAC auf einem Bild der US-Armee
68
+
69
+ Der EDVAC
70
+
71
+ RΓΆhrenrechner Ural-1 aus der Sowjetunion
72
+ Das Ende des Zweiten Weltkriegs erlaubte es, dass EuropÀer und Amerikaner von ihren Fortschritten gegenseitig wieder Kenntnis erlangten. Im Jahr 1946 wurde der Electronical Numerical Integrator and Computer (ENIAC) unter der Leitung von John Eckert und John Mauchly entwickelt und an der Moore School of Electrical Engineering der UniversitÀt von Pennsylvania gebaut. Der ENIAC verfügte über 20 elektronische Register, 3 Funktionstafeln als Festspeicher und bestand aus 18.000 Râhren sowie 1.500 Relais.[4] Der ENIAC ist der erste vollelektronische digitale Universalrechner (Konrad Zuses Z3 verwendete 1941 noch Relais, war also nicht vollelektronisch). 1947 baute IBM den Selective Sequence Electronic Calculator (SSEC), einen Hybridcomputer mit Râhren und mechanischen Relais und die Association for Computing Machinery (ACM) wurde als erste wissenschaftliche Gesellschaft für Informatik gegründet. Im gleichen Jahr wurde auch der erste Transistor realisiert, der heute aus der modernen Technik nicht mehr weggedacht werden kann. Die maßgeblich an der Erfindung beteiligten William B. Shockley, John Bardeen und Walter Brattain erhielten 1956 den Nobelpreis für Physik. In die spÀten 1940er Jahre fÀllt auch der Bau des Electronic Discrete Variable Automatic Computer (EDVAC), der erstmals die Von-Neumann-Architektur implementierte.
73
+
74
+ Im Jahr 1949 stellte Edmund C. Berkeley, BegrΓΌnder der ACM, mit β€žSimonβ€œ den ersten digitalen, programmierbaren Computer fΓΌr den Heimgebrauch vor. Er bestand aus 50 Relais und wurde in Gestalt von BauplΓ€nen vertrieben, von denen in den ersten zehn Jahren ihrer VerfΓΌgbarkeit ΓΌber 400 Exemplare verkauft wurden. Im selben Jahr stellte Maurice Wilkes mit seinem Team in Cambridge den Electronic Delay Storage Automatic Calculator (EDSAC) vor; basierend auf John von Neumanns EDVAC ist es der erste Rechner, der vollstΓ€ndig speicherprogrammierbar war. Ebenfalls 1949 besichtigte der Schweizer Mathematikprofessor Eduard Stiefel die in einem Pferdestall in Hopferau aufgestellte Zuse Z4 und finanzierte die grΓΌndliche Überholung der Maschine durch die Zuse KG, bevor sie an die ETH ZΓΌrich ausgeliefert wurde und dort in Betrieb ging.[5]
75
+
76
+ 1950er
77
+ In den 1950er Jahren setzte die Produktion kommerzieller (Serien-)Computer ein. Unter der Leitung von Alwin Walther wurde am Institut fΓΌr Praktische Mathematik (IPM) der TH Darmstadt ab 1951 der DERA (DarmstΓ€dter Elektronischer Rechenautomat) erbaut. Remington Rand baute 1951 ihren ersten kommerziellen RΓΆhrenrechner, den UNIVersal Automatic Computer I (UNIVAC I) und 1955 bei Bell Labs fΓΌr die US Air Force nimmt der von Jean Howard Felker und L.C. Brown (Charlie Braun) gebaute TRansistorized Airborne DIgital Computer (TRADIC) den ersten Computer der Welt, der komplett mit Transistoren statt RΓΆhren bestΓΌckt war den Betrieb auf; im gleichen Jahr begann Heinz Zemanek mit der Konstruktion des ersten auf europΓ€ischem Festland gebauten Transistorrechners, des MailΓΌfterls, das er 1958 der Γ–ffentlichkeit vorstellte.
78
+
79
+ Am 30. Dezember 1954[6] vollendet die DDR mit der β€žOPtik-REchen-MAschineβ€œ (OPREMA) den Bau ihres ersten Computers mit Hilfe von Relais, der zunΓ€chst als Doppelrechner aus zwei identischen Systemen redundant ausgelegt wurde. Als klar war, dass die Maschinen stabil arbeiteten, wurden sie in zwei unabhΓ€ngige Rechner getrennt. Programmierung und Zahleneingabe wurden per Stecktafel vorgenommen, die Ausgabe erfolgte ΓΌber eine Schreibmaschine.[7] 1956 tauchte der Begriff β€žComputerβ€œ erstmals in der DDR-Presse auf, nΓ€mlich im Zusammenhang mit dem Eniac-β€žRechenautomatenβ€œ, dessen Akronym fΓΌr β€žElectronic Numerical Integrator and Computerβ€œ stand.[8][9] GelΓ€ufig wurde der Begriff erst Mitte der 1960er Jahre.
80
+
81
+ 1956 nahm die ETH ZΓΌrich ihre ERMETH in Betrieb und IBM fertigte das erste Magnetplattensystem (Random Access Method of Accounting and Control (RAMAC)). Ab 1958 wurde die Electrologica X1 als volltransistorisierter Serienrechner gebaut. Noch im selben Jahr stellte die Polnische Akademie der Wissenschaften in Zusammenarbeit mit dem Laboratorium fΓΌr mathematische Apparate unter der Leitung von Romuald Marczynski den ersten polnischen Digital Computer β€žXYZβ€œ vor. Vorgesehenes Einsatzgebiet war die Nuklearforschung. 1959 begann Siemens mit der Auslieferung des Siemens 2002, ihres ersten in Serie gefertigten und vollstΓ€ndig auf Basis von Transistoren hergestellten Computers.
82
+
83
+ 1960er
84
+ 1960 baute IBM den IBM 1401, einen transistorisierten Rechner mit Magnetbandsystem, und DECs (Digital Equipment Corporation) erster Minicomputer, die PDP-1 (Programmierbarer Datenprozessor) erscheint. 1962 lieferte die Telefunken AG die ersten TR 4 aus. 1964 baute DEC den Minicomputer PDP-8 fΓΌr unter 20.000 Dollar.
85
+
86
+ 1964 definierte IBM die erste Computerarchitektur S/360, womit Rechner verschiedener Leistungsklassen denselben Code ausfΓΌhren konnten und bei Texas Instruments wurde der erste β€žintegrierte Schaltkreisβ€œ (IC) entwickelt. 1965 stellte das Moskauer Institut fΓΌr PrΓ€zisionsmechanik und Computertechnologie unter der Leitung seines Chefentwicklers Sergej Lebedjew mit dem BESM-6 den ersten exportfΓ€higen Großcomputer der UdSSR vor. BESM-6 wurde ab 1967 mit Betriebssystem und Compiler ausgeliefert und bis 1987 gebaut. 1966 erschien dann auch noch mit D4a ein 33-Bit-Auftischrechner der TU Dresden.
87
+
88
+
89
+ Olivetti Programma 101
90
+ Der erste frei programmierbare Tischrechner der Welt, der β€žProgramma 101β€œ von der Firma Olivetti,[10] erschien 1965 fΓΌr einen Preis von $3,200[11] (was auf das Jahr 2017 bezogen $24,746[12] entspricht).
91
+
92
+
93
+ Nixdorf 820 von 1968
94
+ 1968 bewarb Hewlett-Packard (HP) den HP-9100A in der Science-Ausgabe vom 4. Oktober 1968 als β€žpersonal computerβ€œ, obgleich diese Bezeichnung nichts mit dem zu tun hat, was seit Mitte der 1970er Jahre bis heute unter einem Personal Computer verstanden wird. Die 1968 entstandene Nixdorf Computer AG erschloss zunΓ€chst in Deutschland und Europa, spΓ€ter auch in Nordamerika, einen neuen Computermarkt: die Mittlere Datentechnik bzw. die dezentrale elektronische Datenverarbeitung. Massenhersteller wie IBM setzten weiterhin auf Großrechner und zentralisierte Datenverarbeitung, wobei Großrechner fΓΌr kleine und mittlere Unternehmen schlicht zu teuer waren und die Großhersteller den Markt der Mittleren Datentechnik nicht bedienen konnten. Nixdorf stieß in diese Marktnische mit dem modular aufgebauten Nixdorf 820 vor, brachte dadurch den Computer direkt an den Arbeitsplatz und ermΓΆglichte kleinen und mittleren Betrieben die Nutzung der elektronischen Datenverarbeitung zu einem erschwinglichen Preis. Im Dezember 1968 stellten Douglas C. Engelbart und William English vom Stanford Research Institute (SRI) die erste Computermaus vor, mangels sinnvoller EinsatzmΓΆglichkeit (es gab noch keine grafischen BenutzeroberflΓ€chen) interessierte dies jedoch kaum jemanden. 1969 werden die ersten Computer per Internet verbunden.
95
+
96
+ 1970er
97
+ Mit der Erfindung des serienmÀßig produzierbaren Mikroprozessors wurden die Computer immer kleiner, leistungsfΓ€higer und preisgΓΌnstiger. Doch noch wurde das Potential der Computer verkannt. So sagte noch 1977 Ken Olson, PrΓ€sident und GrΓΌnder von DEC: β€žEs gibt keinen Grund, warum jemand einen Computer zu Hause haben wollte.β€œ
98
+
99
+
100
+ Intel 8008, VorlΓ€ufer des Intel 8080
101
+ Im Jahr 1971 war es der Hersteller Intel, der mit dem 4004 den ersten in Serie gefertigten Mikroprozessor baute. Er bestand aus 2250 Transistoren. 1971 lieferte Telefunken den TR 440 an das Deutsche Rechenzentrum Darmstadt sowie an die UniversitÀten Bochum und München. 1972 ging der Illiac IV, ein Supercomputer mit Array-Prozessoren, in Betrieb. 1973 erschien mit Xerox Alto der erste Computer mit Maus, graphischer BenutzeroberflÀche (GUI) und eingebauter Ethernet-Karte; und die franzâsische Firma R2E begann mit der Auslieferung des Micral. 1974 stellte HP mit dem HP-65 den ersten programmierbaren Taschenrechner vor und Motorola baute den 6800-Prozessor, wÀhrenddessen Intel den 8080-Prozessor fertigte. 1975 begann MITS mit der Auslieferung des Altair 8800. 1975 stellte IBM mit der IBM 5100 den ersten tragbaren Computer vor. Eine ZeichenlÀnge von 8 Bit und die Einengung der (schon existierenden) Bezeichnung Byte auf dieses Maß wurden in dieser Zeit gelÀufig.
102
+
103
+ 1975 Maestro I (ursprΓΌnglich Programm-Entwicklungs-Terminal-System PET) von Softlab war weltweit die erste Integrierte Entwicklungsumgebung fΓΌr Software. Maestro I wurde weltweit 22.000 Mal installiert, davon 6.000 Mal in der Bundesrepublik Deutschland. Maestro I war in den 1970er und 1980er Jahren fΓΌhrend auf diesem Gebiet.
104
+
105
+
106
+ Zilog Z80
107
+ 1976 entwickelte Zilog den Z80-Prozessor und Apple Computer stellte den Apple I vor, den weltweit ersten Personal Computer,[13][14][15] gefolgt 1977 vom Commodore PET und dem Tandy TRS-80. Der ebenfalls im Jahr 1977 verâffentlichte Apple II gilt bislang als letzter in Serie hergestellter Computer, der von einer einzelnen Person, Steve Wozniak, entworfen wurde.[16] 1978 erschien die VAX-11/780 von DEC, eine Maschine speziell für virtuelle Speicheradressierung. Im gleichen Jahr stellte Intel den 8086 vor, ein 16-Bit-Mikroprozessor; er ist der Urvater der noch heute gebrÀuchlichen x86-Prozessor-Familie. 1979 schließlich startete Atari den Verkauf seiner Rechnermodelle 400 und 800. RevolutionÀr war bei diesen, dass mehrere ASIC-Chips den Hauptprozessor entlasteten.
108
+
109
+ 1980er
110
+
111
+ C64 mit 5ΒΌβ€³-Diskette und Laufwerk
112
+ Die 1980er waren die BlΓΌtezeit der Heimcomputer, zunΓ€chst mit 8-Bit-Mikroprozessoren und einem Arbeitsspeicher bis 64 KiB (Commodore VC20, C64, Sinclair ZX80/81, Sinclair ZX Spectrum, Schneider/Amstrad CPC 464/664, Atari XL/XE-Reihe), spΓ€ter auch leistungsfΓ€higere Modelle mit 16-Bit- (Texas Instruments TI-99/4A) oder 16/32-Bit-Mikroprozessoren (z. B. Amiga, Atari ST). Eine Eigenentwicklung von Siemens, der Siemens PC 16-10, war dagegen mit einem Anfangspreis von 11.300 DM deutlich zu teuer.
113
+
114
+ Das Unternehmen IBM stellte 1981 den IBM-PC vor, legte die Grundkonstruktion offen und schuf einen informellen Industriestandard;[17] sie definierten damit die bis heute aktuelle GerΓ€teklasse der β€žIBM-PC-kompatiblen Computerβ€œ. Dank zahlreicher preiswerter Nachbauten und FortfΓΌhrungen wurde diese GerΓ€teklasse zu einer der erfolgreichsten Plattformen fΓΌr den Personal Computer; die heute marktΓΌblichen PCs mit Windows-Betriebssystem und x86-Prozessoren beruhen auf der stetigen Weiterentwicklung des damaligen Entwurfs von IBM.
115
+
116
+ 1982 brachte Intel den 80286-Prozessor auf den Markt und Sun Microsystems entwickelte die Sun-1 Workstation. Nach dem ersten BΓΌro-Computer mit Maus, Lisa, der 1983 auf den Markt kam, wurde 1984 der Apple Macintosh gebaut und setzte neue MaßstΓ€be fΓΌr Benutzerfreundlichkeit. Die Sowjetunion konterte mit ihrem β€žKronos 1β€œ, einer Bastelarbeit des Rechenzentrums in Akademgorodok. Im Januar 1985 stellte Atari den ST-Computer auf der Consumer Electronics Show (CES) in Las Vegas vor. Im Juli produzierte Commodore den ersten Amiga-Heimcomputer. In Sibirien wurde der β€žKronos 2β€œ vorgestellt, der dann als β€žKronos 2.6β€œ fΓΌr vier Jahre in Serie ging. 1986 brachte Intel den 80386-Prozessor auf den Markt, 1989 den 80486. Ebenfalls 1986 prΓ€sentierte Motorola den 68030-Prozessor. Im gleichen Jahr stellte Acorn den ARM2-Prozessor fertig und setzte ihn im Folgejahr in Acorn-Archimedes-Rechnern ein. 1988 stellte NeXT mit Steve Jobs, MitgrΓΌnder von Apple, den gleichnamigen Computer vor.
117
+
118
+ Die Computer-Fernvernetzung, deutsch β€žDFΓœβ€œ (DatenfernΓΌbertragung), ΓΌber das Usenet wurde an UniversitΓ€ten und in diversen Firmen immer stΓ€rker benutzt. Auch Privatleute strebten nun eine Vernetzung ihrer Computer an; Mitte der 1980er Jahre entstanden Mailboxnetze, zusΓ€tzlich zum FidoNet das Z-Netz und das MausNet.
119
+
120
+ 1990er
121
+
122
+ Pentium
123
+ Die 1990er sind das Jahrzehnt des Internets und des World Wide Web. (Siehe auch Geschichte des Internets, Chronologie des Internets) 1991 spezifizierte das AIM-Konsortium (Apple, IBM, Motorola) die PowerPC-Plattform. 1992 stellte DEC die ersten Systeme mit dem 64-Bit-Alpha-Prozessor vor. 1993 brachte Intel den Pentium-Prozessor auf den Markt, 1995 den Pentium Pro. 1994 stellte Leonard Adleman mit dem TT-100 den ersten Prototyp eines DNA-Computers vor, im Jahr darauf Be Incorporated die BeBox. 1999 baute Intel den Supercomputer ASCI Red mit 9.472 Prozessoren und AMD stellte mit dem Athlon den Nachfolger der K6-Prozessorfamilie vor.
124
+
125
+ Entwicklung im 21. Jahrhundert
126
+ Zu Beginn des 21. Jahrhunderts sind Computer sowohl in beruflichen wie privaten Bereichen allgegenwΓ€rtig und allgemein akzeptiert. WΓ€hrend die LeistungsfΓ€higkeit in klassischen Anwendungsbereichen weiter gesteigert wird, werden digitale Rechner unter anderem in die Telekommunikation und Bildbearbeitung integriert. 2001 baute IBM den Supercomputer ASCI White, und 2002 ging der NEC Earth Simulator in Betrieb. 2003 lieferte Apple den PowerMac G5 aus, den ersten Computer mit 64-Bit-Prozessoren fΓΌr den Massenmarkt. AMD zog mit dem Opteron und dem Athlon 64 nach.
127
+
128
+ 2005 produzierten AMD und Intel erste Dual-Core-Prozessoren, 2006 doppelte Intel mit den ersten Core-2-Quad-Prozessoren nach – AMD konnte erst 2007 erste Vierkernprozessoren vorstellen. Bis zum Jahr 2010 stellten mehrere Firmen auch Sechs- und Achtkernprozessoren vor. Entwicklungen wie Mehrkernprozessoren, Berechnung auf Grafikprozessoren (GPGPU) sowie der breite Einsatz von Tablet-Computern dominieren in den letzten Jahren (Stand 2012) das Geschehen.
129
+
130
+ Seit den 1980er Jahren stiegen die Taktfrequenzen von anfangs wenigen MHz bis zuletzt (Stand 2015) etwa 4 GHz. In den letzten Jahren konnte der Takt nur noch wenig gesteigert werden, stattdessen wurden Steigerungen der Rechenleistung eher durch mehr Prozessorkerne und vergrâßerte Busbreiten erzielt. Auch wenn durch Übertaktung einzelne Prozessoren auf über 8 GHz betrieben werden konnten, sind diese Taktraten auch 2015 noch nicht in Serienprozessoren verfügbar. Außerdem werden zunehmend auch die in Computern verbauten Grafikprozessoren zur Erhâhung der Rechenleistung für spezielle Aufgaben genutzt (z. B. per OpenCL, siehe auch Streamprozessor und GPGPU).
131
+
132
+ Seit ca. 2005 spielen auch Umweltaspekte (wie z. B. Stromsparfunktionen von Prozessor und Chipsatz, verringerter Einsatz schΓ€dlicher Stoffe) – bei der Produktion, Beschaffung und Nutzung von Computern zunehmend eine Rolle (siehe auch Green IT).
133
+
134
+ 2020
135
+ Ende September 2020 wurde Europas letztes Computerwerk, Fujitsu in Augsburg, geschlossen.[18]
136
+
137
+ Siehe auch
138
+ Commons: Geschichte des Computers – Sammlung von Bildern, Videos und Audiodateien
139
+ Elektronikschrott
140
+ Liste historischer Rechenanlagen in Europa
141
+ Liste von Computermuseen
142
+ Mediengeschichte
143
+ Technikgeschichte
144
+ Überwachung
145
+ Literatur
146
+ Edmund Callis Berkeley: Giant Brains or Machines That Think. 7. Auflage. John Wiley & Sons 1949, New York 1963 (die erste populΓ€re Darstellung der EDV, trotz des fΓΌr moderne Ohren seltsam klingenden Titels sehr seriΓΆs und fundiert – relativ einfach antiquarisch und in fast allen Bibliotheken zu finden).
147
+ Frank BΓΆsch (Hrsg.): Wege in die digitale Gesellschaft. Computernutzung in der Bundesrepublik 1955-1990, Wallstein, GΓΆttingen 2018
148
+ Bertram Vivian Bowden (Hrsg.): Faster Than Thought. Pitman, New York 1953 (Nachdruck 1963, ISBN 0-273-31580-3) – eine frΓΌhe populΓ€re Darstellung der EDV, gibt den Stand seiner Zeit verstΓ€ndlich und ausfΓΌhrlich wieder; nur mehr antiquarisch und in Bibliotheken zu finden
149
+ Herbert Bruderer: Meilensteine der Rechentechnik. Band 1: Mechanische Rechenmaschinen, Rechenschieber, historische Automaten und wissenschaftliche Instrumente, 2., stark erweiterte Auflage, Walter de Gruyter, Berlin/Boston 2018, ISBN 978-3-11-051827-6
150
+ Michael Friedewald: Der Computer als Werkzeug und Medium. Die geistigen und technischen Wurzeln des Personalcomputers. GNT-Verlag, 2000, ISBN 3-928186-47-7.
151
+ Thomas Haigh: Jenseits der Genies. Geschichten aus der IT-Arbeit, mandelbaum, Wien 2025
152
+ Thomas Haigh, Mark Priestley, Crispin Rope: ENIAC in Action: Making and Remaking the Modern Computer, MIT Press, Cambridge, Mass. 2016[19]
153
+ Thomas Haigh, Paul E. Ceruzzi: A New History of Modern Computing (History of Computing), MIT Press, Cambridge, Massachusetts ; London, England 2021, 544 S. [aktualisierte Ausgabe des Standardwerks]
154
+ Simon Head: The New Ruthless Economy. Work and Power in the Digital Age. Oxford UP 2005, ISBN 0-19-517983-8 (der Einsatz des Computers in der Tradition des Taylorismus).
155
+ Ute Hoffmann: Computerfrauen. Welchen Anteil hatten Frauen an der Computergeschichte und -arbeit? MΓΌnchen 1987, ISBN 3-924346-30-5
156
+ Loading History. Computergeschichte(n) aus der Schweiz. Museum fΓΌr Kommunikation, Bern 2001, ISBN 3-0340-0540-7, Ausstellungskatalog zu einer Sonderausstellung mit Schweizer Schwerpunkt, aber fΓΌr sich alleine lesbar
157
+ Michael Homberg: Digitale UnabhΓ€ngigkeit: Indiens Weg ins Computerzeitalter – Eine internationale Geschichte (Geschichte der Gegenwart), Wallstein, GΓΆttingen 2022
158
+ Anthony Hyman: Charles Babbage. Pioneer of the Computer. Oxford University Press, Oxford 1984.
159
+ HNF Heinz Nixdorf Forum MuseumsfΓΌhrer. Paderborn 2000, ISBN 3-9805757-2-1 – MuseumsfΓΌhrer des nach eigener Darstellung weltgrâßten Computermuseums
160
+ Jens MΓΌller: The Computer. A History from the 17th Century to Today. Hrsg.: Julius Wiedemann. Taschen Verlag, 2023, ISBN 978-3-8365-7334-4 (englisch).
161
+ AndrΓ© Reifenrath: Geschichte der Simulation. Dissertation Humboldt-UniversitΓ€t Berlin 2000. Geschichte des Computers von den AnfΓ€ngen bis zur Gegenwart unter besonderer BerΓΌcksichtigung des Themas der Visualisierung und Simulation durch den Computer.
162
+ Claude E. Shannon: A Symbolic Analysis of Relay and Switching Circuits. In: Transactions of the American Institute of Electrical Engineers. Vol. 57, 1938, S. 713–723.
163
+ Karl Weinhart: Informatik und Automatik. FΓΌhrer durch die Ausstellungen. Deutsches Museum, MΓΌnchen 1990, ISBN 3-924183-14-7 – Katalog zu den permanenten Ausstellungen des Deutschen Museums zum Thema; vor allem als ergΓ€nzende Literatur zum Ausstellungsbesuch empfohlen
164
+ H. R. Wieland: Computergeschichte(n) – nicht nur fΓΌr Geeks: Von Antikythera zur Cloud. Galileo Computing, 2010, ISBN 978-3-8362-1527-5
165
+ JΓΌrgen Wolf: Computergeschichte(n): Nicht nur fΓΌr Nerds. Eine Zeitreise durch die IT-Geschichte. Rheinwerk Computing, Bonn 2020, ISBN 978-3-8362-7777-8.
166
+ Joseph Weizenbaum: Computer Power and Human Reason. From Judgement to Calculation. W. H. Freeman and Company, Freeman, San Francisco CA 1976, ISBN 0-7167-0464-1 (Deutsch als: Die Macht der Computer und die Ohnmacht der Vernunft. Suhrkamp, Frankfurt am Main 1977, ISBN 3-518-27874-6 (Suhrkamp Taschenbuch Wissenschaft 274); zahlreiche Auflagen).
167
+ Christian Wurster: Computers. Eine illustrierte Geschichte. Taschen, 2002, ISBN 3-8228-5729-7 (eine vom Text her leider nicht sehr exakte Geschichte der EDV mit einzelnen Fehlern, die aber durch die GastbeitrΓ€ge einzelner PersΓΆnlichkeiten der Computergeschichte und durch die zahlreichen Fotos ihren Wert hat).
168
+ Shoshana Zuboff: Das Zeitalter des Überwachungskapitalismus, Campus, Frankfurt am Main 2025 (Paperback)
169
+ Einzelnachweise
170
+ UCL: Experts recreate a mechanical Cosmos for the world’s first computer. 12. MΓ€rz 2021, abgerufen am 18. MΓ€rz 2021 (englisch).
171
+ Konrad Zuse: Die Erfindung des Computers. In: swr.de. 17. Mai 1984, abgerufen am 25. August 2020.
172
+ Klaus Schmeh: Als deutscher Code-Knacker im Zweiten Weltkrieg. In: heise.de. 24. September 2004, abgerufen am 25. August 2020.
173
+ Wilfried de Beauclair: Rechnen mit Maschinen. Eine Bildgeschichte der Rechentechnik. 2. Auflage. Springer, Berlin Heidelberg New York 2005, ISBN 3-540-24179-5, S. 111–113.
174
+ Stefan Betschon: Der Zauber des Anfangs. Schweizer Computerpioniere. In: Franz Betschon, Stefan Betschon, JΓΌrg Lindecker, Willy Schlachter (Hrsg.): Ingenieure bauen die Schweiz. Technikgeschichte aus erster Hand. Verlag Neue ZΓΌrcher Zeitung, ZΓΌrich 2013, ISBN 978-3-03823-791-4, S. 376–399.
175
+ RenΓ© Meyer: Computer in der DDR. Robotron statt Commodore, VEB RΓΆhrenwerk MΓΌhlhausen statt IBM: Die Computertechnik in der DDR war ganz anders als im Westen – vor allem viel abenteuerlicher. In: www.heise.de. Heise.de, 12. Januar 2023, abgerufen am 12. Januar 2023.
176
+ Andreas GΓΆbel: Spiegel Geschichte: Mit diesem Monstrum konnte man rechnen. 14. Juni 2013, abgerufen im Jahr 2020.
177
+ Neues Deutschland, 6. Mai 1956
178
+ Erich Sobeslavsky, Nikolaus Joachim Lehmann: Rechentechnik und Datenverarbeitung in der DDR - 1946 bis 1968. (PDF) Hannah-Arendt-Institut TU Dresden, 1996, abgerufen im Jahr 2020.
179
+ siehe K. Dette: Olivetti Personal Computer fur Lehre und Forschung. Springer, 1989; Brennan, AnnMarie: Olivetti: A work of art in the age of immaterial labour. In: Journal of Design History 28.3 (2015): 235–253; Tischcomputer. In: kuno.de
180
+ Wobbe Vegter: Cyber Heroes of the past: Camillo Olivetti. 11. MΓ€rz 2009, abgerufen am 6. April 2017 (englisch).
181
+ US Inflation Calculator
182
+ Steven Levy: Hackers: Heroes of the Computer Revolution. Doubleday 1984, ISBN 0-385-19195-2
183
+ Boris GrΓΆndahl: Hacker. Rotbuch 3000, ISBN 3-434-53506-3
184
+ Steve Wozniak: iWoz: Wie ich den Personal Computer erfand und Apple mitgrΓΌndete. Deutscher Taschenbuchverlag, Oktober 2008, ISBN 978-3-423-34507-1
185
+ Der Traum vom einfachen Computer. In: Der Tagesspiegel
186
+ Frank Patalong: 30 Jahre IBM-PC: Siegeszug der WenigkΓΆnner. In: spiegel.de. 12. August 2011, abgerufen am 21. August 2016.
187
+ Europas letztes Computerwerk schließt – Fujitsu macht Augsburg dicht. In: derstandard.de. 26. Oktober 2018, abgerufen am 2. Februar 2024.
188
+ prozessurale Perspektive, auch methodisch wegweisende Studie, vgl. die Rezension in β€žBerichte zur Wissenschaftsgeschichteβ€œ 40(2017), S. 98–100
189
+ Kategorien: ComputerGeschichte der Informatik
190
+ Diese Seite wurde zuletzt am 19. April 2026 um 13:50 Uhr bearbeitet. Die Seite wurde
docs/screenshots/node-a-ask-tab.png CHANGED
docs/screenshots/node-b-settings-tab.png CHANGED
hearthnet/blobs/chunker.py CHANGED
@@ -3,7 +3,6 @@ from __future__ import annotations
3
  import hashlib
4
  import json
5
  from dataclasses import dataclass
6
- from typing import Optional
7
 
8
  CHUNK_SIZE_BYTES = 256 * 1024 # 256 KB
9
 
 
3
  import hashlib
4
  import json
5
  from dataclasses import dataclass
 
6
 
7
  CHUNK_SIZE_BYTES = 256 * 1024 # 256 KB
8
 
hearthnet/blobs/transfer.py CHANGED
@@ -51,7 +51,9 @@ class TransferManager:
51
  resp.raise_for_status()
52
  raw = resp.json()
53
  except Exception as exc:
54
- raise BlobError("io_error", f"Failed to fetch manifest from {manifest_url}: {exc}") from exc
 
 
55
 
56
  from hearthnet.blobs.store import BlobStore as _BS
57
 
 
51
  resp.raise_for_status()
52
  raw = resp.json()
53
  except Exception as exc:
54
+ raise BlobError(
55
+ "io_error", f"Failed to fetch manifest from {manifest_url}: {exc}"
56
+ ) from exc
57
 
58
  from hearthnet.blobs.store import BlobStore as _BS
59
 
hearthnet/bus/router.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""M03 - Capability Bus - Router.
2
 
3
  Spec: docs/M03-bus.md Β§3.5 (routing) Β§5.4 (scoring algorithm)
4
  Impl-ref: impl_ref.md Β§7 Router
@@ -6,6 +6,7 @@ Impl-ref: impl_ref.md Β§7 Router
6
  Scoring: latency-weighted success rate, capacity headroom, prefer local.
7
  Quarantine threshold: HEALTH_QUARANTINE_THRESHOLD (hearthnet/constants.py).
8
  """
 
9
  from __future__ import annotations
10
 
11
  import time
@@ -81,4 +82,3 @@ def _score(entry: CapabilityEntry) -> float:
81
  reliability_penalty = (1.0 - entry.success_rate) * 1000
82
  locality_bonus = -50 if entry.is_local else 0
83
  return latency * (1 + load) + reliability_penalty + locality_bonus
84
-
 
1
+ """M03 - Capability Bus - Router.
2
 
3
  Spec: docs/M03-bus.md Β§3.5 (routing) Β§5.4 (scoring algorithm)
4
  Impl-ref: impl_ref.md Β§7 Router
 
6
  Scoring: latency-weighted success rate, capacity headroom, prefer local.
7
  Quarantine threshold: HEALTH_QUARANTINE_THRESHOLD (hearthnet/constants.py).
8
  """
9
+
10
  from __future__ import annotations
11
 
12
  import time
 
82
  reliability_penalty = (1.0 - entry.success_rate) * 1000
83
  locality_bonus = -50 if entry.is_local else 0
84
  return latency * (1 + load) + reliability_penalty + locality_bonus
 
hearthnet/bus/schema.py CHANGED
@@ -1,4 +1,5 @@
1
  """JSON Schema validation for capability requests/responses."""
 
2
  from __future__ import annotations
3
 
4
  import hashlib
@@ -42,9 +43,7 @@ class SchemaValidator:
42
  key = f"resp:{descriptor.name}:{descriptor.version_str}"
43
  self._validate(descriptor.response_schema, response, key)
44
 
45
- def validate_stream_frame(
46
- self, descriptor: CapabilityDescriptor, frame: dict
47
- ) -> None:
48
  """Validate a streaming frame."""
49
  if not HAS_JSONSCHEMA or not descriptor.stream_schema:
50
  return
@@ -62,9 +61,7 @@ class SchemaValidator:
62
 
63
  def compute_schema_hash(descriptor_partial: dict) -> str:
64
  """SHA-256 (or BLAKE3 if available) over canonical-JSON of descriptor."""
65
- canonical = json.dumps(
66
- descriptor_partial, sort_keys=True, separators=(",", ":")
67
- ).encode()
68
  try:
69
  import blake3 # type: ignore[import]
70
 
 
1
  """JSON Schema validation for capability requests/responses."""
2
+
3
  from __future__ import annotations
4
 
5
  import hashlib
 
43
  key = f"resp:{descriptor.name}:{descriptor.version_str}"
44
  self._validate(descriptor.response_schema, response, key)
45
 
46
+ def validate_stream_frame(self, descriptor: CapabilityDescriptor, frame: dict) -> None:
 
 
47
  """Validate a streaming frame."""
48
  if not HAS_JSONSCHEMA or not descriptor.stream_schema:
49
  return
 
61
 
62
  def compute_schema_hash(descriptor_partial: dict) -> str:
63
  """SHA-256 (or BLAKE3 if available) over canonical-JSON of descriptor."""
64
+ canonical = json.dumps(descriptor_partial, sort_keys=True, separators=(",", ":")).encode()
 
 
65
  try:
66
  import blake3 # type: ignore[import]
67
 
hearthnet/bus/trace.py CHANGED
@@ -1,4 +1,5 @@
1
  """Bus trace events for call tracking."""
 
2
  from __future__ import annotations
3
 
4
  from dataclasses import dataclass
 
1
  """Bus trace events for call tracking."""
2
+
3
  from __future__ import annotations
4
 
5
  from dataclasses import dataclass
hearthnet/civdef/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
  """M31 β€” Civil Defense package (experimental, Phase 3)."""
 
2
  from __future__ import annotations
3
 
4
  from hearthnet.civdef.service import Alert, AuditChain, CivilDefenseService, RoleCertificate
 
1
  """M31 β€” Civil Defense package (experimental, Phase 3)."""
2
+
3
  from __future__ import annotations
4
 
5
  from hearthnet.civdef.service import Alert, AuditChain, CivilDefenseService, RoleCertificate
hearthnet/civdef/service.py CHANGED
@@ -4,6 +4,7 @@ Bridges HearthNet with THW/DRK/Feuerwehr/KatS role structures.
4
  Produces tamper-evident audit trails for incident coordination.
5
  Gated by config.research.civil_defense = True.
6
  """
 
7
  from __future__ import annotations
8
 
9
  import hashlib
@@ -30,16 +31,17 @@ NRW_ROLES = {
30
  @dataclass(frozen=True)
31
  class AlertSeverity:
32
  INFORMATION = "information"
33
- WARNING = "warning"
34
- ALERT = "alert"
35
- EMERGENCY = "emergency"
36
 
37
 
38
  @dataclass(frozen=True)
39
  class RoleCertificate:
40
  """A role certificate issued by an authority for a community member."""
 
41
  cert_id: str
42
- role_key: str # key from NRW_ROLES
43
  role_label: str
44
  holder_node_id: str
45
  issuer_node_id: str
@@ -61,15 +63,16 @@ class RoleCertificate:
61
  @dataclass(frozen=True)
62
  class Alert:
63
  """A civil-defense alert with full provenance."""
 
64
  alert_id: str
65
- severity: str # AlertSeverity constant
66
  title: str
67
  body: str
68
- area_description: str # e.g. "Issum, Kreis Kleve, NRW"
69
  issuer_node_id: str
70
  issuer_role_cert_id: str | None
71
  community_id: str
72
- event_log_id: str | None = None # optional backlink to event log entry
73
  issued_at: float = field(default_factory=time.time)
74
  expires_at: float | None = None
75
  issuer_signature: bytes = b""
@@ -168,25 +171,28 @@ class CivilDefenseService:
168
  expires_at=time.time() + expires_in_hours * 3600 if expires_in_hours else None,
169
  )
170
  self._alerts[alert.alert_id] = alert
171
- self._audit.append("alert.issued", node_id, {
172
- "alert_id": alert.alert_id,
173
- "severity": alert.severity,
174
- "title": alert.title,
175
- })
 
 
 
 
176
  return alert
177
 
178
  def list_active_alerts(self, now: float | None = None) -> list[Alert]:
179
  now = now or time.time()
180
- return [
181
- a for a in self._alerts.values()
182
- if a.expires_at is None or a.expires_at > now
183
- ]
184
 
185
  def register_cert(self, cert: RoleCertificate) -> None:
186
  self._certs[cert.cert_id] = cert
187
- self._audit.append("cert.registered", cert.issuer_node_id, {
188
- "cert_id": cert.cert_id, "role": cert.role_key, "holder": cert.holder_node_id
189
- })
 
 
190
 
191
  def verify_cert(self, cert_id: str) -> dict:
192
  cert = self._certs.get(cert_id)
 
4
  Produces tamper-evident audit trails for incident coordination.
5
  Gated by config.research.civil_defense = True.
6
  """
7
+
8
  from __future__ import annotations
9
 
10
  import hashlib
 
31
  @dataclass(frozen=True)
32
  class AlertSeverity:
33
  INFORMATION = "information"
34
+ WARNING = "warning"
35
+ ALERT = "alert"
36
+ EMERGENCY = "emergency"
37
 
38
 
39
  @dataclass(frozen=True)
40
  class RoleCertificate:
41
  """A role certificate issued by an authority for a community member."""
42
+
43
  cert_id: str
44
+ role_key: str # key from NRW_ROLES
45
  role_label: str
46
  holder_node_id: str
47
  issuer_node_id: str
 
63
  @dataclass(frozen=True)
64
  class Alert:
65
  """A civil-defense alert with full provenance."""
66
+
67
  alert_id: str
68
+ severity: str # AlertSeverity constant
69
  title: str
70
  body: str
71
+ area_description: str # e.g. "Issum, Kreis Kleve, NRW"
72
  issuer_node_id: str
73
  issuer_role_cert_id: str | None
74
  community_id: str
75
+ event_log_id: str | None = None # optional backlink to event log entry
76
  issued_at: float = field(default_factory=time.time)
77
  expires_at: float | None = None
78
  issuer_signature: bytes = b""
 
171
  expires_at=time.time() + expires_in_hours * 3600 if expires_in_hours else None,
172
  )
173
  self._alerts[alert.alert_id] = alert
174
+ self._audit.append(
175
+ "alert.issued",
176
+ node_id,
177
+ {
178
+ "alert_id": alert.alert_id,
179
+ "severity": alert.severity,
180
+ "title": alert.title,
181
+ },
182
+ )
183
  return alert
184
 
185
  def list_active_alerts(self, now: float | None = None) -> list[Alert]:
186
  now = now or time.time()
187
+ return [a for a in self._alerts.values() if a.expires_at is None or a.expires_at > now]
 
 
 
188
 
189
  def register_cert(self, cert: RoleCertificate) -> None:
190
  self._certs[cert.cert_id] = cert
191
+ self._audit.append(
192
+ "cert.registered",
193
+ cert.issuer_node_id,
194
+ {"cert_id": cert.cert_id, "role": cert.role_key, "holder": cert.holder_node_id},
195
+ )
196
 
197
  def verify_cert(self, cert_id: str) -> dict:
198
  cert = self._certs.get(cert_id)
hearthnet/cli.py CHANGED
@@ -1,4 +1,5 @@
1
  """HearthNet CLI β€” `hearthnet` command."""
 
2
  from __future__ import annotations
3
 
4
  import asyncio
@@ -61,7 +62,9 @@ def _http_post(url: str, body: str) -> dict:
61
  try:
62
  import httpx
63
 
64
- resp = httpx.post(url, content=body, headers={"Content-Type": "application/json"}, timeout=30)
 
 
65
  resp.raise_for_status()
66
  return resp.json()
67
  except ImportError:
@@ -93,7 +96,9 @@ def _http_post(url: str, body: str) -> dict:
93
 
94
  @click.group()
95
  @click.version_option(version="0.1.0")
96
- @click.option("--config", "config_path", type=click.Path(), default=None, help="Path to config.toml")
 
 
97
  @click.pass_context
98
  def main(ctx: click.Context, config_path: str | None) -> None:
99
  """HearthNet β€” community-owned local AI mesh."""
 
1
  """HearthNet CLI β€” `hearthnet` command."""
2
+
3
  from __future__ import annotations
4
 
5
  import asyncio
 
62
  try:
63
  import httpx
64
 
65
+ resp = httpx.post(
66
+ url, content=body, headers={"Content-Type": "application/json"}, timeout=30
67
+ )
68
  resp.raise_for_status()
69
  return resp.json()
70
  except ImportError:
 
96
 
97
  @click.group()
98
  @click.version_option(version="0.1.0")
99
+ @click.option(
100
+ "--config", "config_path", type=click.Path(), default=None, help="Path to config.toml"
101
+ )
102
  @click.pass_context
103
  def main(ctx: click.Context, config_path: str | None) -> None:
104
  """HearthNet β€” community-owned local AI mesh."""
hearthnet/config.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""X04 - Configuration.
2
 
3
  Spec: docs/X04-config.md
4
  Impl-ref: impl_ref.md Β§1
@@ -20,13 +20,13 @@ Example config.toml:
20
  url = "http://localhost:8000"
21
  model = "openbmb/MiniCPM4-8B"
22
  """
 
23
  from __future__ import annotations
24
 
25
  import os
26
  import tomllib # stdlib Ò‰Β₯ 3.11; fallback below
27
  from dataclasses import dataclass, field
28
  from pathlib import Path
29
- from typing import Optional
30
 
31
  from hearthnet.constants import (
32
  CHUNK_SIZE_BYTES,
@@ -49,6 +49,7 @@ except ImportError:
49
 
50
  # Ò”€Ò”€ Sub-config dataclasses Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
51
 
 
52
  @dataclass(frozen=True)
53
  class IdentityConfig:
54
  keys_dir: Path = field(default_factory=lambda: Path())
@@ -160,6 +161,7 @@ class ObservabilityConfig:
160
  @dataclass(frozen=True)
161
  class ResearchConfig:
162
  """Phase 3 experimental feature flags. All default False."""
 
163
  enable: bool = False
164
  distributed_inference: bool = False
165
  moe_routing: bool = False
@@ -191,6 +193,7 @@ class Config:
191
 
192
  # Ò”€Ò”€ ConfigError Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
193
 
 
194
  class ConfigError(Exception):
195
  def __init__(self, code: str, **kwargs: object) -> None:
196
  super().__init__(code)
@@ -200,6 +203,7 @@ class ConfigError(Exception):
200
 
201
  # Ò”€Ò”€ XDG path resolution Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
202
 
 
203
  def _xdg_data() -> Path:
204
  raw = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
205
  return Path(raw) / "hearthnet"
@@ -221,6 +225,7 @@ def _default_config_path() -> Path:
221
 
222
  # Ò”€Ò”€ Path resolution Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
223
 
 
224
  def resolve_paths(config: Config) -> Config:
225
  """Fill empty Path() fields with XDG-standard locations. Idempotent."""
226
  data = _xdg_data()
@@ -293,22 +298,28 @@ def resolve_paths(config: Config) -> Config:
293
 
294
  # Ò”€Ò”€ Validation Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
295
 
 
296
  def validate(config: Config) -> None:
297
  """Cross-field validation. Raises ConfigError on failure."""
298
  t = config.transport
299
  d = config.discovery
300
  if t.port == d.udp_port:
301
- raise ConfigError("invalid_field", field="transport.port/discovery.udp_port",
302
- reason="transport port and UDP discovery port must differ")
 
 
 
303
  if not (1 <= t.port <= 65535):
304
  raise ConfigError("invalid_field", field="transport.port", reason="port out of range")
305
  if config.bus.local_load_threshold <= 0 or config.bus.local_load_threshold > 1:
306
- raise ConfigError("invalid_field", field="bus.local_load_threshold",
307
- reason="must be in (0, 1]")
 
308
 
309
 
310
  # Ò”€Ò”€ TOML parsing helpers Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
311
 
 
312
  def _parse_toml(text: str) -> dict:
313
  if tomllib is None:
314
  raise ConfigError("invalid_toml", reason="no TOML parser available (install tomli)")
@@ -359,12 +370,14 @@ def _from_dict(raw: dict) -> Config:
359
  llm_raw = raw.get("llm", {})
360
  backends = []
361
  for b in llm_raw.get("backends", []):
362
- backends.append(LlmBackendConfig(
363
- name=str(b["name"]),
364
- model=str(b.get("model", "")),
365
- base_url=str(b.get("base_url", "")),
366
- api_key_env=b.get("api_key_env") or None,
367
- ))
 
 
368
  llm = LlmConfig(backends=tuple(backends))
369
 
370
  embedding_raw = raw.get("embedding", {})
@@ -402,9 +415,17 @@ def _from_dict(raw: dict) -> Config:
402
 
403
  emergency_raw = raw.get("emergency", {})
404
  emergency = EmergencyConfig(
405
- probe_targets=tuple(emergency_raw.get("probe_targets", [
406
- "1.1.1.1", "8.8.8.8", "https://cloudflare.com", "https://quad9.net",
407
- ])),
 
 
 
 
 
 
 
 
408
  )
409
 
410
  ui_raw = raw.get("ui", {})
@@ -442,6 +463,7 @@ def _from_dict(raw: dict) -> Config:
442
 
443
  # Ò”€Ò”€ Public API Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
444
 
 
445
  def default_config() -> Config:
446
  """Return a Config populated entirely from defaults."""
447
  return resolve_paths(Config())
@@ -527,4 +549,3 @@ def save(config: Config, path: Path | None = None) -> None:
527
  except OSError:
528
  pass
529
  raise
530
-
 
1
+ """X04 - Configuration.
2
 
3
  Spec: docs/X04-config.md
4
  Impl-ref: impl_ref.md Β§1
 
20
  url = "http://localhost:8000"
21
  model = "openbmb/MiniCPM4-8B"
22
  """
23
+
24
  from __future__ import annotations
25
 
26
  import os
27
  import tomllib # stdlib Ò‰Β₯ 3.11; fallback below
28
  from dataclasses import dataclass, field
29
  from pathlib import Path
 
30
 
31
  from hearthnet.constants import (
32
  CHUNK_SIZE_BYTES,
 
49
 
50
  # Ò”€Ò”€ Sub-config dataclasses Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
51
 
52
+
53
  @dataclass(frozen=True)
54
  class IdentityConfig:
55
  keys_dir: Path = field(default_factory=lambda: Path())
 
161
  @dataclass(frozen=True)
162
  class ResearchConfig:
163
  """Phase 3 experimental feature flags. All default False."""
164
+
165
  enable: bool = False
166
  distributed_inference: bool = False
167
  moe_routing: bool = False
 
193
 
194
  # Ò”€Ò”€ ConfigError Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
195
 
196
+
197
  class ConfigError(Exception):
198
  def __init__(self, code: str, **kwargs: object) -> None:
199
  super().__init__(code)
 
203
 
204
  # Ò”€Ò”€ XDG path resolution Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
205
 
206
+
207
  def _xdg_data() -> Path:
208
  raw = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
209
  return Path(raw) / "hearthnet"
 
225
 
226
  # Ò”€Ò”€ Path resolution Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
227
 
228
+
229
  def resolve_paths(config: Config) -> Config:
230
  """Fill empty Path() fields with XDG-standard locations. Idempotent."""
231
  data = _xdg_data()
 
298
 
299
  # Ò”€Ò”€ Validation Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
300
 
301
+
302
  def validate(config: Config) -> None:
303
  """Cross-field validation. Raises ConfigError on failure."""
304
  t = config.transport
305
  d = config.discovery
306
  if t.port == d.udp_port:
307
+ raise ConfigError(
308
+ "invalid_field",
309
+ field="transport.port/discovery.udp_port",
310
+ reason="transport port and UDP discovery port must differ",
311
+ )
312
  if not (1 <= t.port <= 65535):
313
  raise ConfigError("invalid_field", field="transport.port", reason="port out of range")
314
  if config.bus.local_load_threshold <= 0 or config.bus.local_load_threshold > 1:
315
+ raise ConfigError(
316
+ "invalid_field", field="bus.local_load_threshold", reason="must be in (0, 1]"
317
+ )
318
 
319
 
320
  # Ò”€Ò”€ TOML parsing helpers Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
321
 
322
+
323
  def _parse_toml(text: str) -> dict:
324
  if tomllib is None:
325
  raise ConfigError("invalid_toml", reason="no TOML parser available (install tomli)")
 
370
  llm_raw = raw.get("llm", {})
371
  backends = []
372
  for b in llm_raw.get("backends", []):
373
+ backends.append(
374
+ LlmBackendConfig(
375
+ name=str(b["name"]),
376
+ model=str(b.get("model", "")),
377
+ base_url=str(b.get("base_url", "")),
378
+ api_key_env=b.get("api_key_env") or None,
379
+ )
380
+ )
381
  llm = LlmConfig(backends=tuple(backends))
382
 
383
  embedding_raw = raw.get("embedding", {})
 
415
 
416
  emergency_raw = raw.get("emergency", {})
417
  emergency = EmergencyConfig(
418
+ probe_targets=tuple(
419
+ emergency_raw.get(
420
+ "probe_targets",
421
+ [
422
+ "1.1.1.1",
423
+ "8.8.8.8",
424
+ "https://cloudflare.com",
425
+ "https://quad9.net",
426
+ ],
427
+ )
428
+ ),
429
  )
430
 
431
  ui_raw = raw.get("ui", {})
 
463
 
464
  # Ò”€Ò”€ Public API Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
465
 
466
+
467
  def default_config() -> Config:
468
  """Return a Config populated entirely from defaults."""
469
  return resolve_paths(Config())
 
549
  except OSError:
550
  pass
551
  raise
 
hearthnet/constants.py CHANGED
@@ -3,6 +3,7 @@
3
  All module code that needs a tunable default imports from here.
4
  Never hardcode these values inline.
5
  """
 
6
  from __future__ import annotations
7
 
8
  # ── Node manifest ────────────────────────────────────────────────────────────
@@ -28,7 +29,7 @@ RATE_LIMIT_WINDOW_SECONDS: int = 60
28
  RATE_LIMIT_MAX_CALLS: int = 200
29
 
30
  # ── Bus ──────────────────────────────────────────────────────────────────────
31
- BUS_HEALTH_WINDOW: int = 20 # samples per ring-buffer window
32
  BUS_QUARANTINE_SECONDS: int = 60
33
  BUS_FRESHNESS_SECONDS: int = 60
34
  BUS_LOCAL_LOAD_THRESHOLD: float = 0.80
@@ -54,7 +55,7 @@ LOG_RETENTION_DAYS: int = 14
54
  TRACE_RING_BUFFER_SIZE: int = 1000
55
 
56
  # ── Onboarding ───────────────────────────────────────────────────────────────
57
- INVITE_DEFAULT_TTL_SECONDS: int = 86400 # 24 h
58
 
59
  # ── RAG / Embedding ──────────────────────────────────────────────────────────
60
  RAG_DEFAULT_CHUNK_SIZE_TOKENS: int = 512
@@ -65,8 +66,6 @@ RERANK_LOAD_TIMEOUT_SECONDS: int = 60
65
  EMBED_MAX_TEXTS: int = 256
66
  EMBED_MAX_CHARS: int = 8192
67
  RAG_OVERLAP_TOKENS: int = 64
68
- EMBED_MAX_TEXTS: int = 256
69
- EMBED_MAX_CHARS: int = 8192
70
  EMBED_DEFAULT_MODEL: str = "BAAI/bge-small-en-v1.5"
71
 
72
  # ── LLM ──────────────────────────────────────────────────────────────────────
@@ -74,8 +73,8 @@ LLM_STREAM_CANCEL_TIMEOUT_MS: int = 200
74
 
75
  # ── Marketplace ──────────────────────────────────────────────────────────────
76
  MARKET_SWEEP_INTERVAL_SECONDS: int = 60
77
- MARKET_DEFAULT_TTL_SECONDS: int = 86400 * 7 # 1 week
78
- MARKET_MAX_TTL_SECONDS: int = 86400 * 30 # 30 days
79
  MARKET_SEARCH_CACHE_MAX: int = 5000
80
 
81
  # ── STT / TTS ─────────────────────────────────────────────────────────────────
@@ -83,6 +82,3 @@ STT_MAX_AUDIO_SECONDS: int = 300
83
 
84
  # ── Translation ───────────────────────────────────────────────────────────────
85
  TRANSLATION_MAX_CHARS: int = 4000
86
-
87
- # ── Rerank ────────────────────────────────────────────────────────────────────
88
- RERANK_MAX_DOCS: int = 100
 
3
  All module code that needs a tunable default imports from here.
4
  Never hardcode these values inline.
5
  """
6
+
7
  from __future__ import annotations
8
 
9
  # ── Node manifest ────────────────────────────────────────────────────────────
 
29
  RATE_LIMIT_MAX_CALLS: int = 200
30
 
31
  # ── Bus ──────────────────────────────────────────────────────────────────────
32
+ BUS_HEALTH_WINDOW: int = 20 # samples per ring-buffer window
33
  BUS_QUARANTINE_SECONDS: int = 60
34
  BUS_FRESHNESS_SECONDS: int = 60
35
  BUS_LOCAL_LOAD_THRESHOLD: float = 0.80
 
55
  TRACE_RING_BUFFER_SIZE: int = 1000
56
 
57
  # ── Onboarding ───────────────────────────────────────────────────────────────
58
+ INVITE_DEFAULT_TTL_SECONDS: int = 86400 # 24 h
59
 
60
  # ── RAG / Embedding ──────────────────────────────────────────────────────────
61
  RAG_DEFAULT_CHUNK_SIZE_TOKENS: int = 512
 
66
  EMBED_MAX_TEXTS: int = 256
67
  EMBED_MAX_CHARS: int = 8192
68
  RAG_OVERLAP_TOKENS: int = 64
 
 
69
  EMBED_DEFAULT_MODEL: str = "BAAI/bge-small-en-v1.5"
70
 
71
  # ── LLM ──────────────────────────────────────────────────────────────────────
 
73
 
74
  # ── Marketplace ──────────────────────────────────────────────────────────────
75
  MARKET_SWEEP_INTERVAL_SECONDS: int = 60
76
+ MARKET_DEFAULT_TTL_SECONDS: int = 86400 * 7 # 1 week
77
+ MARKET_MAX_TTL_SECONDS: int = 86400 * 30 # 30 days
78
  MARKET_SEARCH_CACHE_MAX: int = 5000
79
 
80
  # ── STT / TTS ─────────────────────────────────────────────────────────────────
 
82
 
83
  # ── Translation ───────────────────────────────────────────────────────────────
84
  TRANSLATION_MAX_CHARS: int = 4000
 
 
 
hearthnet/crypto/envelope.py CHANGED
@@ -1,4 +1,5 @@
1
  """File-chunk envelope encryption for HearthNet blobs (M23 / M07 extension)."""
 
2
  from __future__ import annotations
3
 
4
  import hashlib
@@ -53,7 +54,7 @@ class EncryptedEnvelope:
53
 
54
  ciphertext: bytes
55
  nonce: bytes # 24 bytes (XSalsa20 nonce)
56
- key_id: str # identifies which key was used (e.g., recipient node_id or blob CID)
57
 
58
 
59
  # ---------------------------------------------------------------------------
@@ -65,9 +66,7 @@ def envelope_encrypt(plaintext: bytes, key: bytes) -> EncryptedEnvelope:
65
  """Encrypt plaintext with XSalsa20-Poly1305 using the given 32-byte key."""
66
  _require_nacl()
67
  if len(key) != nacl.secret.SecretBox.KEY_SIZE:
68
- raise CryptoError(
69
- f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes, got {len(key)}"
70
- )
71
  box = nacl.secret.SecretBox(key)
72
  nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
73
  ciphertext = bytes(box.encrypt(plaintext, nonce).ciphertext)
@@ -78,9 +77,7 @@ def envelope_decrypt(envelope: EncryptedEnvelope, key: bytes) -> bytes:
78
  """Decrypt an EncryptedEnvelope using the given 32-byte key."""
79
  _require_nacl()
80
  if len(key) != nacl.secret.SecretBox.KEY_SIZE:
81
- raise CryptoError(
82
- f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes, got {len(key)}"
83
- )
84
  box = nacl.secret.SecretBox(key)
85
  try:
86
  return bytes(box.decrypt(envelope.ciphertext, envelope.nonce))
 
1
  """File-chunk envelope encryption for HearthNet blobs (M23 / M07 extension)."""
2
+
3
  from __future__ import annotations
4
 
5
  import hashlib
 
54
 
55
  ciphertext: bytes
56
  nonce: bytes # 24 bytes (XSalsa20 nonce)
57
+ key_id: str # identifies which key was used (e.g., recipient node_id or blob CID)
58
 
59
 
60
  # ---------------------------------------------------------------------------
 
66
  """Encrypt plaintext with XSalsa20-Poly1305 using the given 32-byte key."""
67
  _require_nacl()
68
  if len(key) != nacl.secret.SecretBox.KEY_SIZE:
69
+ raise CryptoError(f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes, got {len(key)}")
 
 
70
  box = nacl.secret.SecretBox(key)
71
  nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
72
  ciphertext = bytes(box.encrypt(plaintext, nonce).ciphertext)
 
77
  """Decrypt an EncryptedEnvelope using the given 32-byte key."""
78
  _require_nacl()
79
  if len(key) != nacl.secret.SecretBox.KEY_SIZE:
80
+ raise CryptoError(f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes, got {len(key)}")
 
 
81
  box = nacl.secret.SecretBox(key)
82
  try:
83
  return bytes(box.decrypt(envelope.ciphertext, envelope.nonce))
hearthnet/crypto/kem.py CHANGED
@@ -1,4 +1,5 @@
1
  """X25519 key agreement + X3DH for HearthNet E2E encryption (M23)."""
 
2
  from __future__ import annotations
3
 
4
  import base64
@@ -220,9 +221,7 @@ def x3dh_initiator(
220
  "ephemeral_pub": _b64url_encode(our_ephemeral_kp.public),
221
  "signed_prekey_pub": _b64url_encode(their_bundle.signed_prekey_pub),
222
  "used_otp_index": used_otp_index,
223
- "used_otp_pub": (
224
- their_bundle.one_time_prekeys[0] if used_otp_index is not None else None
225
- ),
226
  }
227
  return shared_secret, session_init_message
228
 
 
1
  """X25519 key agreement + X3DH for HearthNet E2E encryption (M23)."""
2
+
3
  from __future__ import annotations
4
 
5
  import base64
 
221
  "ephemeral_pub": _b64url_encode(our_ephemeral_kp.public),
222
  "signed_prekey_pub": _b64url_encode(their_bundle.signed_prekey_pub),
223
  "used_otp_index": used_otp_index,
224
+ "used_otp_pub": (their_bundle.one_time_prekeys[0] if used_otp_index is not None else None),
 
 
225
  }
226
  return shared_secret, session_init_message
227
 
hearthnet/crypto/prekeys.py CHANGED
@@ -1,4 +1,5 @@
1
  """Prekey bundle storage for HearthNet E2E encryption (M23)."""
 
2
  from __future__ import annotations
3
 
4
  import base64
 
1
  """Prekey bundle storage for HearthNet E2E encryption (M23)."""
2
+
3
  from __future__ import annotations
4
 
5
  import base64
hearthnet/crypto/ratchet.py CHANGED
@@ -1,4 +1,5 @@
1
  """Double Ratchet session for HearthNet E2E encryption (M23)."""
 
2
  from __future__ import annotations
3
 
4
  import base64
 
1
  """Double Ratchet session for HearthNet E2E encryption (M23)."""
2
+
3
  from __future__ import annotations
4
 
5
  import base64
hearthnet/dht/bootstrap.py CHANGED
@@ -1,7 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import json
4
- from dataclasses import dataclass, field
5
  from pathlib import Path
6
  from typing import Any
7
 
@@ -41,7 +41,7 @@ def load_bootstrap(config_path: str | Path | None = None) -> BootstrapConfig:
41
 
42
  # Auto-discover relay_url from XDG config if possible
43
  try:
44
- from hearthnet.config import _default_config_path, load # noqa: PLC0415
45
 
46
  cfg_file = _default_config_path()
47
  if cfg_file.is_file():
 
1
  from __future__ import annotations
2
 
3
  import json
4
+ from dataclasses import dataclass
5
  from pathlib import Path
6
  from typing import Any
7
 
 
41
 
42
  # Auto-discover relay_url from XDG config if possible
43
  try:
44
+ from hearthnet.config import _default_config_path, load
45
 
46
  cfg_file = _default_config_path()
47
  if cfg_file.is_file():
hearthnet/dht/kademlia.py CHANGED
@@ -2,23 +2,22 @@ from __future__ import annotations
2
 
3
  import hashlib
4
  import time
5
- from dataclasses import dataclass, field
6
- from typing import Any
7
 
8
 
9
  @dataclass(frozen=True)
10
  class DhtContact:
11
- node_key: bytes # 32-byte SHA-256 of node_id
12
- endpoint: str # "host:port"
13
- node_id: str # human-readable node identifier
14
- last_seen: float # monotonic timestamp
15
 
16
 
17
  @dataclass(frozen=True)
18
  class DhtValue:
19
- key: bytes # lookup key (arbitrary bytes)
20
- payload: dict # stored data
21
- expires_at: int # Unix epoch seconds
22
 
23
 
24
  def _xor_distance(a: bytes, b: bytes) -> int:
@@ -30,7 +29,7 @@ def _xor_distance(a: bytes, b: bytes) -> int:
30
  elif lb < la:
31
  b = b.ljust(la, b"\x00")
32
  result = 0
33
- for x, y in zip(a, b):
34
  result = (result << 8) | (x ^ y)
35
  return result
36
 
 
2
 
3
  import hashlib
4
  import time
5
+ from dataclasses import dataclass
 
6
 
7
 
8
  @dataclass(frozen=True)
9
  class DhtContact:
10
+ node_key: bytes # 32-byte SHA-256 of node_id
11
+ endpoint: str # "host:port"
12
+ node_id: str # human-readable node identifier
13
+ last_seen: float # monotonic timestamp
14
 
15
 
16
  @dataclass(frozen=True)
17
  class DhtValue:
18
+ key: bytes # lookup key (arbitrary bytes)
19
+ payload: dict # stored data
20
+ expires_at: int # Unix epoch seconds
21
 
22
 
23
  def _xor_distance(a: bytes, b: bytes) -> int:
 
29
  elif lb < la:
30
  b = b.ljust(la, b"\x00")
31
  result = 0
32
+ for x, y in zip(a, b, strict=False):
33
  result = (result << 8) | (x ^ y)
34
  return result
35
 
hearthnet/discovery/__init__.py CHANGED
@@ -3,7 +3,11 @@ from hearthnet.discovery.peers import PeerEvent, PeerRecord, PeerRegistry
3
  from hearthnet.discovery.udp import UdpAnnouncer, UdpListener
4
 
5
  __all__ = [
6
- "PeerRecord", "PeerRegistry", "PeerEvent",
7
- "MdnsAnnouncer", "MdnsBrowser",
8
- "UdpAnnouncer", "UdpListener",
 
 
 
 
9
  ]
 
3
  from hearthnet.discovery.udp import UdpAnnouncer, UdpListener
4
 
5
  __all__ = [
6
+ "MdnsAnnouncer",
7
+ "MdnsBrowser",
8
+ "PeerEvent",
9
+ "PeerRecord",
10
+ "PeerRegistry",
11
+ "UdpAnnouncer",
12
+ "UdpListener",
13
  ]
hearthnet/discovery/mdns.py CHANGED
@@ -7,6 +7,7 @@ import time
7
  try:
8
  from zeroconf import ServiceInfo
9
  from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf
 
10
  HAS_ZEROCONF = True
11
  except ImportError:
12
  HAS_ZEROCONF = False
@@ -40,6 +41,7 @@ class MdnsAnnouncer:
40
  return
41
  try:
42
  import socket
 
43
  self._zeroconf = AsyncZeroconf()
44
  short = self._node_id.replace("ed25519:", "")[:8]
45
  name = f"{self._display_name[:20]}-{short}.{MDNS_SERVICE_TYPE}"
@@ -98,6 +100,7 @@ class MdnsBrowser:
98
  async def _handle_change(self, zeroconf, service_type, name, state_change) -> None:
99
  try:
100
  from zeroconf import ServiceStateChange
 
101
  if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
102
  info = await zeroconf.async_get_service_info(service_type, name)
103
  if info:
@@ -110,6 +113,7 @@ class MdnsBrowser:
110
  if not node_id:
111
  return
112
  import socket
 
113
  addresses = [socket.inet_ntoa(a) for a in info.addresses]
114
  host = addresses[0] if addresses else "127.0.0.1"
115
  record = PeerRecord(
 
7
  try:
8
  from zeroconf import ServiceInfo
9
  from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf
10
+
11
  HAS_ZEROCONF = True
12
  except ImportError:
13
  HAS_ZEROCONF = False
 
41
  return
42
  try:
43
  import socket
44
+
45
  self._zeroconf = AsyncZeroconf()
46
  short = self._node_id.replace("ed25519:", "")[:8]
47
  name = f"{self._display_name[:20]}-{short}.{MDNS_SERVICE_TYPE}"
 
100
  async def _handle_change(self, zeroconf, service_type, name, state_change) -> None:
101
  try:
102
  from zeroconf import ServiceStateChange
103
+
104
  if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
105
  info = await zeroconf.async_get_service_info(service_type, name)
106
  if info:
 
113
  if not node_id:
114
  return
115
  import socket
116
+
117
  addresses = [socket.inet_ntoa(a) for a in info.addresses]
118
  host = addresses[0] if addresses else "127.0.0.1"
119
  record = PeerRecord(
hearthnet/discovery/peers.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""M02 - Peer discovery: PeerRegistry.
2
 
3
  Spec: docs/M02-discovery.md Β§3.1
4
  Impl-ref: impl_ref.md Β§6
@@ -6,12 +6,14 @@ Impl-ref: impl_ref.md Β§6
6
  Holds PeerRecord entries discovered via mDNS or UDP multicast.
7
  Async subscribe() notifies bus and UI on peer changes.
8
  """
 
9
  from __future__ import annotations
10
 
11
  import asyncio
12
  import time
 
13
  from dataclasses import dataclass, field
14
- from typing import Any, AsyncIterator
15
 
16
  from hearthnet.types import CommunityID, Endpoint, NodeID, Profile
17
 
@@ -94,15 +96,22 @@ class PeerRegistry:
94
  def prune_stale_seconds(self) -> int:
95
  from hearthnet.constants import PEER_PRUNE_AGGRESSIVE_SECONDS, PEER_PRUNE_NORMAL_SECONDS
96
 
97
- return PEER_PRUNE_AGGRESSIVE_SECONDS if self._pruning_aggressive else PEER_PRUNE_NORMAL_SECONDS
 
 
98
 
99
  def prune_stale(self, max_age_seconds: int | None = None) -> int:
100
  """Remove peers whose last_seen is beyond the prune threshold."""
101
  from hearthnet.constants import PEER_PRUNE_AGGRESSIVE_SECONDS, PEER_PRUNE_NORMAL_SECONDS
 
102
  if max_age_seconds is not None:
103
  threshold = max_age_seconds
104
  else:
105
- threshold = PEER_PRUNE_AGGRESSIVE_SECONDS if self._pruning_aggressive else PEER_PRUNE_NORMAL_SECONDS
 
 
 
 
106
  now = time.monotonic()
107
  stale = [nid for nid, peer in self._peers.items() if now - peer.last_seen > threshold]
108
  for nid in stale:
@@ -137,4 +146,3 @@ class PeerRegistry:
137
  q.put_nowait(event)
138
  except asyncio.QueueFull:
139
  pass
140
-
 
1
+ """M02 - Peer discovery: PeerRegistry.
2
 
3
  Spec: docs/M02-discovery.md Β§3.1
4
  Impl-ref: impl_ref.md Β§6
 
6
  Holds PeerRecord entries discovered via mDNS or UDP multicast.
7
  Async subscribe() notifies bus and UI on peer changes.
8
  """
9
+
10
  from __future__ import annotations
11
 
12
  import asyncio
13
  import time
14
+ from collections.abc import AsyncIterator
15
  from dataclasses import dataclass, field
16
+ from typing import Any
17
 
18
  from hearthnet.types import CommunityID, Endpoint, NodeID, Profile
19
 
 
96
  def prune_stale_seconds(self) -> int:
97
  from hearthnet.constants import PEER_PRUNE_AGGRESSIVE_SECONDS, PEER_PRUNE_NORMAL_SECONDS
98
 
99
+ return (
100
+ PEER_PRUNE_AGGRESSIVE_SECONDS if self._pruning_aggressive else PEER_PRUNE_NORMAL_SECONDS
101
+ )
102
 
103
  def prune_stale(self, max_age_seconds: int | None = None) -> int:
104
  """Remove peers whose last_seen is beyond the prune threshold."""
105
  from hearthnet.constants import PEER_PRUNE_AGGRESSIVE_SECONDS, PEER_PRUNE_NORMAL_SECONDS
106
+
107
  if max_age_seconds is not None:
108
  threshold = max_age_seconds
109
  else:
110
+ threshold = (
111
+ PEER_PRUNE_AGGRESSIVE_SECONDS
112
+ if self._pruning_aggressive
113
+ else PEER_PRUNE_NORMAL_SECONDS
114
+ )
115
  now = time.monotonic()
116
  stale = [nid for nid, peer in self._peers.items() if now - peer.last_seen > threshold]
117
  for nid in stale:
 
146
  q.put_nowait(event)
147
  except asyncio.QueueFull:
148
  pass
 
hearthnet/discovery/udp.py CHANGED
@@ -63,14 +63,17 @@ class UdpAnnouncer:
63
  async def _announce_once(self) -> None:
64
  try:
65
  import socket
 
66
  short_id = self._node_id[8:20] if len(self._node_id) > 8 else self._node_id
67
- payload = json.dumps({
68
- "v": 1,
69
- "node": short_id,
70
- "community": self._community_id[:20],
71
- "port": self._port,
72
- "caps": self._caps[:10],
73
- }).encode()
 
 
74
  if len(payload) > 1024:
75
  payload = payload[:1024]
76
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
@@ -112,6 +115,7 @@ class UdpListener:
112
  async def _listen_loop(self) -> None:
113
  import socket
114
  import struct
 
115
  try:
116
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
117
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
63
  async def _announce_once(self) -> None:
64
  try:
65
  import socket
66
+
67
  short_id = self._node_id[8:20] if len(self._node_id) > 8 else self._node_id
68
+ payload = json.dumps(
69
+ {
70
+ "v": 1,
71
+ "node": short_id,
72
+ "community": self._community_id[:20],
73
+ "port": self._port,
74
+ "caps": self._caps[:10],
75
+ }
76
+ ).encode()
77
  if len(payload) > 1024:
78
  payload = payload[:1024]
79
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
 
115
  async def _listen_loop(self) -> None:
116
  import socket
117
  import struct
118
+
119
  try:
120
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
121
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
hearthnet/distributed_inference/__init__.py CHANGED
@@ -3,9 +3,10 @@
3
  Gated by config.research.distributed_inference = True.
4
  Layer-shards an LLM across multiple LAN nodes (Petals-style).
5
  """
 
6
  from __future__ import annotations
7
 
8
- from hearthnet.distributed_inference.shard import ShardDescriptor, ShardServer
9
  from hearthnet.distributed_inference.pipeline import Pipeline, PipelineOrchestrator
 
10
 
11
- __all__ = ["ShardDescriptor", "ShardServer", "Pipeline", "PipelineOrchestrator"]
 
3
  Gated by config.research.distributed_inference = True.
4
  Layer-shards an LLM across multiple LAN nodes (Petals-style).
5
  """
6
+
7
  from __future__ import annotations
8
 
 
9
  from hearthnet.distributed_inference.pipeline import Pipeline, PipelineOrchestrator
10
+ from hearthnet.distributed_inference.shard import ShardDescriptor, ShardServer
11
 
12
+ __all__ = ["Pipeline", "PipelineOrchestrator", "ShardDescriptor", "ShardServer"]
hearthnet/distributed_inference/pipeline.py CHANGED
@@ -1,4 +1,5 @@
1
  """Pipeline orchestrator for distributed inference (M26 β€” experimental)."""
 
2
  from __future__ import annotations
3
 
4
  import time
@@ -10,11 +11,12 @@ from hearthnet.distributed_inference.shard import ShardDescriptor
10
  @dataclass
11
  class Pipeline:
12
  """A planned pipeline: ordered list of shards covering layers 0..N."""
 
13
  pipeline_id: str
14
  model_id: str
15
  shards: list[ShardDescriptor]
16
  established_at: float = field(default_factory=time.time)
17
- status: str = "planned" # "planned" | "active" | "failed" | "done"
18
 
19
  @property
20
  def is_complete(self) -> bool:
@@ -50,6 +52,7 @@ class PipelineOrchestrator:
50
  def plan(self, model_id: str, available_shards: list[ShardDescriptor]) -> Pipeline | None:
51
  """Choose a minimal set of shards that covers layers 0..N continuously."""
52
  import uuid
 
53
  model_shards = [s for s in available_shards if s.model_id == model_id]
54
  if not model_shards:
55
  return None
 
1
  """Pipeline orchestrator for distributed inference (M26 β€” experimental)."""
2
+
3
  from __future__ import annotations
4
 
5
  import time
 
11
  @dataclass
12
  class Pipeline:
13
  """A planned pipeline: ordered list of shards covering layers 0..N."""
14
+
15
  pipeline_id: str
16
  model_id: str
17
  shards: list[ShardDescriptor]
18
  established_at: float = field(default_factory=time.time)
19
+ status: str = "planned" # "planned" | "active" | "failed" | "done"
20
 
21
  @property
22
  def is_complete(self) -> bool:
 
52
  def plan(self, model_id: str, available_shards: list[ShardDescriptor]) -> Pipeline | None:
53
  """Choose a minimal set of shards that covers layers 0..N continuously."""
54
  import uuid
55
+
56
  model_shards = [s for s in available_shards if s.model_id == model_id]
57
  if not model_shards:
58
  return None
hearthnet/distributed_inference/shard.py CHANGED
@@ -1,7 +1,7 @@
1
  """Shard descriptors and server for distributed inference (M26 β€” experimental)."""
 
2
  from __future__ import annotations
3
 
4
- import asyncio
5
  import time
6
  from dataclasses import dataclass, field
7
  from typing import Any
@@ -12,13 +12,14 @@ ShardID = str
12
  @dataclass(frozen=True)
13
  class ShardDescriptor:
14
  """Describes one contiguous layer range hosted by a node."""
15
- shard_id: ShardID # "<model_id>:<lo>-<hi>"
 
16
  model_id: str
17
  layer_lo: int
18
- layer_hi: int # inclusive
19
  node_id: str
20
  endpoint: str
21
- dtype: str = "float16" # "float16" | "bfloat16" | "int8"
22
  advertised_at: float = field(default_factory=time.time)
23
 
24
  @property
@@ -52,8 +53,7 @@ class ShardServer:
52
  import torch # noqa: F401
53
  except ImportError as exc:
54
  raise ImportError(
55
- "PyTorch is required for distributed inference. "
56
- "Install: pip install torch"
57
  ) from exc
58
  # Actual weight loading would go here; placeholder for the research prototype.
59
  self._loaded = True
 
1
  """Shard descriptors and server for distributed inference (M26 β€” experimental)."""
2
+
3
  from __future__ import annotations
4
 
 
5
  import time
6
  from dataclasses import dataclass, field
7
  from typing import Any
 
12
  @dataclass(frozen=True)
13
  class ShardDescriptor:
14
  """Describes one contiguous layer range hosted by a node."""
15
+
16
+ shard_id: ShardID # "<model_id>:<lo>-<hi>"
17
  model_id: str
18
  layer_lo: int
19
+ layer_hi: int # inclusive
20
  node_id: str
21
  endpoint: str
22
+ dtype: str = "float16" # "float16" | "bfloat16" | "int8"
23
  advertised_at: float = field(default_factory=time.time)
24
 
25
  @property
 
53
  import torch # noqa: F401
54
  except ImportError as exc:
55
  raise ImportError(
56
+ "PyTorch is required for distributed inference. Install: pip install torch"
 
57
  ) from exc
58
  # Actual weight loading would go here; placeholder for the research prototype.
59
  self._loaded = True
hearthnet/emergency/detector.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""M09 - Emergency Mode Detector.
2
 
3
  Spec: docs/M09-emergency.md Β§3.2
4
  Impl-ref: impl_ref.md Β§14
@@ -7,6 +7,7 @@ Probes DNS+HTTP every EMERGENCY_PROBE_INTERVAL_ONLINE seconds.
7
  Debounce: EMERGENCY_TRANSITION_DEBOUNCE_SECONDS.
8
  On offline: deregisters capabilities with requires_internet=True.
9
  """
 
10
  from __future__ import annotations
11
 
12
  import asyncio
@@ -81,14 +82,15 @@ class Detector:
81
 
82
  async def _probe_all(self) -> dict[str, bool]:
83
  tasks = {
84
- target: asyncio.create_task(self._probe_one(target))
85
- for target in self._probe_targets
86
  }
87
  results: dict[str, bool] = {}
88
  for target, task in tasks.items():
89
  try:
90
- results[target] = await asyncio.wait_for(task, timeout=EMERGENCY_PROBE_TIMEOUT_SECONDS)
91
- except (asyncio.TimeoutError, Exception):
 
 
92
  results[target] = False
93
  return results
94
 
@@ -171,4 +173,3 @@ class Detector:
171
  if self._peers is not None:
172
  self._peers.set_pruning_aggressive(False)
173
  return state
174
-
 
1
+ """M09 - Emergency Mode Detector.
2
 
3
  Spec: docs/M09-emergency.md Β§3.2
4
  Impl-ref: impl_ref.md Β§14
 
7
  Debounce: EMERGENCY_TRANSITION_DEBOUNCE_SECONDS.
8
  On offline: deregisters capabilities with requires_internet=True.
9
  """
10
+
11
  from __future__ import annotations
12
 
13
  import asyncio
 
82
 
83
  async def _probe_all(self) -> dict[str, bool]:
84
  tasks = {
85
+ target: asyncio.create_task(self._probe_one(target)) for target in self._probe_targets
 
86
  }
87
  results: dict[str, bool] = {}
88
  for target, task in tasks.items():
89
  try:
90
+ results[target] = await asyncio.wait_for(
91
+ task, timeout=EMERGENCY_PROBE_TIMEOUT_SECONDS
92
+ )
93
+ except (TimeoutError, Exception):
94
  results[target] = False
95
  return results
96
 
 
173
  if self._peers is not None:
174
  self._peers.set_pruning_aggressive(False)
175
  return state
 
hearthnet/emergency/state.py CHANGED
@@ -2,8 +2,9 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import time
 
5
  from dataclasses import dataclass
6
- from typing import AsyncIterator, Literal
7
 
8
  Mode = Literal["online", "degraded", "offline"]
9
 
 
2
 
3
  import asyncio
4
  import time
5
+ from collections.abc import AsyncIterator
6
  from dataclasses import dataclass
7
+ from typing import Literal
8
 
9
  Mode = Literal["online", "degraded", "offline"]
10
 
hearthnet/events/__init__.py CHANGED
@@ -9,19 +9,19 @@ from .types import Event, EventType, new_ulid
9
 
10
  __all__ = [
11
  "Event",
12
- "EventType",
13
  "EventLog",
14
  "EventLogError",
 
 
15
  "LamportClock",
16
- "ReplayEngine",
17
  "MaterialisedView",
18
- "SnapshotStore",
19
  "Snapshot",
20
- "build_snapshot",
21
- "restore_from_snapshot",
22
  "SyncClient",
23
- "SyncServer",
24
- "HeadsReport",
25
  "SyncResult",
 
 
26
  "new_ulid",
 
27
  ]
 
9
 
10
  __all__ = [
11
  "Event",
 
12
  "EventLog",
13
  "EventLogError",
14
+ "EventType",
15
+ "HeadsReport",
16
  "LamportClock",
 
17
  "MaterialisedView",
18
+ "ReplayEngine",
19
  "Snapshot",
20
+ "SnapshotStore",
 
21
  "SyncClient",
 
 
22
  "SyncResult",
23
+ "SyncServer",
24
+ "build_snapshot",
25
  "new_ulid",
26
+ "restore_from_snapshot",
27
  ]
hearthnet/events/log.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""X02 - Event log (SQLite WAL).
2
 
3
  Spec: docs/X02-events.md Β§3.3
4
  Impl-ref: impl_ref.md Β§3
@@ -7,6 +7,7 @@ All community events signed with author Ed25519 key.
7
  Lamport clock enforces causal ordering.
8
  ReplayEngine drives materialised views (marketplace, chat).
9
  """
 
10
  from __future__ import annotations
11
 
12
  import asyncio
@@ -14,7 +15,7 @@ import json
14
  import sqlite3
15
  import threading
16
  from collections.abc import AsyncIterator
17
- from datetime import datetime, timezone
18
  from pathlib import Path
19
  from typing import Any
20
 
@@ -52,7 +53,7 @@ CREATE TABLE IF NOT EXISTS clock (
52
 
53
 
54
  def _now_utc() -> str:
55
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
56
 
57
 
58
  def _row_to_event(row: tuple[Any, ...]) -> Event:
@@ -126,7 +127,11 @@ def _verify(event: Event, kp_store: Any) -> bool:
126
  import base64
127
 
128
  prefix = "ed25519:"
129
- b64 = event.signature[len(prefix) :] if event.signature.startswith(prefix) else event.signature
 
 
 
 
130
  # pad
131
  padding = 4 - len(b64) % 4
132
  if padding != 4:
@@ -418,4 +423,3 @@ class EventLog:
418
  q.put_nowait(event)
419
  except asyncio.QueueFull:
420
  pass
421
-
 
1
+ """X02 - Event log (SQLite WAL).
2
 
3
  Spec: docs/X02-events.md Β§3.3
4
  Impl-ref: impl_ref.md Β§3
 
7
  Lamport clock enforces causal ordering.
8
  ReplayEngine drives materialised views (marketplace, chat).
9
  """
10
+
11
  from __future__ import annotations
12
 
13
  import asyncio
 
15
  import sqlite3
16
  import threading
17
  from collections.abc import AsyncIterator
18
+ from datetime import UTC, datetime
19
  from pathlib import Path
20
  from typing import Any
21
 
 
53
 
54
 
55
  def _now_utc() -> str:
56
+ return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
57
 
58
 
59
  def _row_to_event(row: tuple[Any, ...]) -> Event:
 
127
  import base64
128
 
129
  prefix = "ed25519:"
130
+ b64 = (
131
+ event.signature[len(prefix) :]
132
+ if event.signature.startswith(prefix)
133
+ else event.signature
134
+ )
135
  # pad
136
  padding = 4 - len(b64) % 4
137
  if padding != 4:
 
423
  q.put_nowait(event)
424
  except asyncio.QueueFull:
425
  pass
 
hearthnet/events/snapshot.py CHANGED
@@ -4,7 +4,7 @@ import base64
4
  import json
5
  import os
6
  from dataclasses import dataclass
7
- from datetime import datetime, timezone
8
  from pathlib import Path
9
  from typing import TYPE_CHECKING, Any
10
 
@@ -17,7 +17,7 @@ _SCHEMA_VERSION = 1
17
 
18
 
19
  def _now_utc() -> str:
20
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
21
 
22
 
23
  def _sign_snapshot(data: bytes, kp: Any) -> str:
@@ -33,14 +33,18 @@ def _sign_snapshot(data: bytes, kp: Any) -> str:
33
  return "ed25519:" + base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode()
34
 
35
 
36
- def _verify_snapshot(snap: "Snapshot", kp_store: Any) -> bool:
37
  if kp_store is None or not snap.signature:
38
  return True
39
  raw = _canonical_snap_bytes(snap)
40
  if hasattr(kp_store, "verify"):
41
  try:
42
  prefix = "ed25519:"
43
- b64 = snap.signature[len(prefix) :] if snap.signature.startswith(prefix) else snap.signature
 
 
 
 
44
  padding = 4 - len(b64) % 4
45
  if padding != 4:
46
  b64 += "=" * padding
@@ -51,7 +55,7 @@ def _verify_snapshot(snap: "Snapshot", kp_store: Any) -> bool:
51
  return True
52
 
53
 
54
- def _canonical_snap_bytes(snap: "Snapshot") -> bytes:
55
  obj = {
56
  "schema_version": snap.schema_version,
57
  "community_id": snap.community_id,
@@ -68,7 +72,7 @@ class Snapshot:
68
  schema_version: int
69
  community_id: str
70
  at_lamport: int
71
- views: dict[str, dict] # view_name -> state dict
72
  issued_at: str
73
  author: str
74
  signature: str
 
4
  import json
5
  import os
6
  from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
  from pathlib import Path
9
  from typing import TYPE_CHECKING, Any
10
 
 
17
 
18
 
19
  def _now_utc() -> str:
20
+ return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
21
 
22
 
23
  def _sign_snapshot(data: bytes, kp: Any) -> str:
 
33
  return "ed25519:" + base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode()
34
 
35
 
36
+ def _verify_snapshot(snap: Snapshot, kp_store: Any) -> bool:
37
  if kp_store is None or not snap.signature:
38
  return True
39
  raw = _canonical_snap_bytes(snap)
40
  if hasattr(kp_store, "verify"):
41
  try:
42
  prefix = "ed25519:"
43
+ b64 = (
44
+ snap.signature[len(prefix) :]
45
+ if snap.signature.startswith(prefix)
46
+ else snap.signature
47
+ )
48
  padding = 4 - len(b64) % 4
49
  if padding != 4:
50
  b64 += "=" * padding
 
55
  return True
56
 
57
 
58
+ def _canonical_snap_bytes(snap: Snapshot) -> bytes:
59
  obj = {
60
  "schema_version": snap.schema_version,
61
  "community_id": snap.community_id,
 
72
  schema_version: int
73
  community_id: str
74
  at_lamport: int
75
+ views: dict[str, dict] # view_name -> state dict
76
  issued_at: str
77
  author: str
78
  signature: str
hearthnet/events/sync.py CHANGED
@@ -168,9 +168,7 @@ class SyncServer:
168
  )
169
 
170
  # Events the requesting peer is missing
171
- missing_for_peer = [
172
- _event_to_dict(e) for e in self._log.since(peer_head + 1)
173
- ]
174
 
175
  return {
176
  "accepted": accepted,
 
168
  )
169
 
170
  # Events the requesting peer is missing
171
+ missing_for_peer = [_event_to_dict(e) for e in self._log.since(peer_head + 1)]
 
 
172
 
173
  return {
174
  "accepted": accepted,
hearthnet/events/types.py CHANGED
@@ -46,12 +46,12 @@ def new_ulid() -> str:
46
 
47
  @dataclass(frozen=True)
48
  class Event:
49
- schema_version: int # always 1
50
- event_id: str # ULID
51
  event_type: EventType
52
  community_id: str
53
- author: str # full node_id
54
  lamport: int
55
  payload: dict[str, Any]
56
- issued_at: str # RFC 3339 UTC
57
- signature: str # "ed25519:<b64url>" or ""
 
46
 
47
  @dataclass(frozen=True)
48
  class Event:
49
+ schema_version: int # always 1
50
+ event_id: str # ULID
51
  event_type: EventType
52
  community_id: str
53
+ author: str # full node_id
54
  lamport: int
55
  payload: dict[str, Any]
56
+ issued_at: str # RFC 3339 UTC
57
+ signature: str # "ed25519:<b64url>" or ""
hearthnet/evidence/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
  """M30 β€” Evidence Graph package (experimental, Phase 3)."""
 
2
  from __future__ import annotations
3
 
4
- from hearthnet.evidence.store import Claim, ClaimID, ClaimSource, ClaimStore, Attestation, Dispute
5
 
6
- __all__ = ["Claim", "ClaimID", "ClaimSource", "ClaimStore", "Attestation", "Dispute"]
 
1
  """M30 β€” Evidence Graph package (experimental, Phase 3)."""
2
+
3
  from __future__ import annotations
4
 
5
+ from hearthnet.evidence.store import Attestation, Claim, ClaimID, ClaimSource, ClaimStore, Dispute
6
 
7
+ __all__ = ["Attestation", "Claim", "ClaimID", "ClaimSource", "ClaimStore", "Dispute"]
hearthnet/evidence/store.py CHANGED
@@ -4,10 +4,10 @@ Content-addressed claim graph alongside the event log.
4
  Events record what happened; claims record what is believed and by whom.
5
  Gated by config.research.evidence_graph = True.
6
  """
 
7
  from __future__ import annotations
8
 
9
  import hashlib
10
- import json
11
  import time
12
  import uuid
13
  from dataclasses import dataclass, field
@@ -20,7 +20,7 @@ SourceID = NewType("SourceID", str)
20
  @dataclass(frozen=True)
21
  class ClaimSource:
22
  source_id: SourceID
23
- source_type: str # "event" | "external" | "ebkh" | "manual"
24
  url: str | None = None
25
  retrieved_at: float | None = None
26
  reliability_score: float = 1.0
@@ -29,11 +29,12 @@ class ClaimSource:
29
  @dataclass(frozen=True)
30
  class Claim:
31
  """An assertion by a node about some fact, with provenance."""
 
32
  claim_id: ClaimID
33
- subject: str # what the claim is about (URI or free text)
34
- predicate: str # what is being claimed
35
- object_: str # the claimed value
36
- asserted_by: str # NodeID of the asserting node
37
  sources: tuple[ClaimSource, ...]
38
  community_id: str
39
  asserted_at: float = field(default_factory=time.time)
@@ -49,6 +50,7 @@ class Claim:
49
  @dataclass(frozen=True)
50
  class Attestation:
51
  """A second node vouches for a claim."""
 
52
  claim_id: ClaimID
53
  attested_by: str
54
  attested_at: float = field(default_factory=time.time)
@@ -58,6 +60,7 @@ class Attestation:
58
  @dataclass(frozen=True)
59
  class Dispute:
60
  """A node disputes a claim."""
 
61
  claim_id: ClaimID
62
  disputed_by: str
63
  reason: str
@@ -101,7 +104,9 @@ class ClaimStore:
101
  def is_disputed(self, claim_id: ClaimID) -> bool:
102
  return bool(self._disputes.get(claim_id))
103
 
104
- def import_ebkh_record(self, record: dict[str, Any], asserted_by: str, community_id: str) -> ClaimID:
 
 
105
  """Import a record from Christof's EBKH system as a Claim.
106
 
107
  Expects record to have at minimum: subject, predicate, object, source_url.
 
4
  Events record what happened; claims record what is believed and by whom.
5
  Gated by config.research.evidence_graph = True.
6
  """
7
+
8
  from __future__ import annotations
9
 
10
  import hashlib
 
11
  import time
12
  import uuid
13
  from dataclasses import dataclass, field
 
20
  @dataclass(frozen=True)
21
  class ClaimSource:
22
  source_id: SourceID
23
+ source_type: str # "event" | "external" | "ebkh" | "manual"
24
  url: str | None = None
25
  retrieved_at: float | None = None
26
  reliability_score: float = 1.0
 
29
  @dataclass(frozen=True)
30
  class Claim:
31
  """An assertion by a node about some fact, with provenance."""
32
+
33
  claim_id: ClaimID
34
+ subject: str # what the claim is about (URI or free text)
35
+ predicate: str # what is being claimed
36
+ object_: str # the claimed value
37
+ asserted_by: str # NodeID of the asserting node
38
  sources: tuple[ClaimSource, ...]
39
  community_id: str
40
  asserted_at: float = field(default_factory=time.time)
 
50
  @dataclass(frozen=True)
51
  class Attestation:
52
  """A second node vouches for a claim."""
53
+
54
  claim_id: ClaimID
55
  attested_by: str
56
  attested_at: float = field(default_factory=time.time)
 
60
  @dataclass(frozen=True)
61
  class Dispute:
62
  """A node disputes a claim."""
63
+
64
  claim_id: ClaimID
65
  disputed_by: str
66
  reason: str
 
104
  def is_disputed(self, claim_id: ClaimID) -> bool:
105
  return bool(self._disputes.get(claim_id))
106
 
107
+ def import_ebkh_record(
108
+ self, record: dict[str, Any], asserted_by: str, community_id: str
109
+ ) -> ClaimID:
110
  """Import a record from Christof's EBKH system as a Claim.
111
 
112
  Expects record to have at minimum: subject, predicate, object, source_url.
hearthnet/federation/manifest.py CHANGED
@@ -1,4 +1,5 @@
1
  """Federation manifest builder and verifier (M14)."""
 
2
  from __future__ import annotations
3
 
4
  import base64
@@ -44,14 +45,14 @@ class FederationManifest:
44
  community_a_name: str
45
  community_b_id: str
46
  community_b_name: str
47
- scope_a_to_b: FederationScope # what A grants B
48
- scope_b_to_a: FederationScope # what B grants A
49
- sig_a: str # Ed25519 sig from anchor of community A
50
- sig_b: str # Ed25519 sig from anchor of community B
51
- co_signers_a: list[str] # additional anchor signatures from community A
52
- co_signers_b: list[str] # additional anchor signatures from community B
53
- created_at: int # unix seconds
54
- expires_at: int # unix seconds
55
  bootstrap_endpoints_a: list[str]
56
  bootstrap_endpoints_b: list[str]
57
 
@@ -64,14 +65,14 @@ class FederationManifest:
64
  class FederationProposal:
65
  """A draft federation proposal from community A to community B."""
66
 
67
- community_a: str # community_id of proposer
68
- community_b: str # community_id of target
69
- scope_a: FederationScope # scope A proposes to grant B
70
- scope_b: FederationScope # scope A requests from B
71
- bootstrap_a: list[str] # endpoints for community A
72
- bootstrap_b: list[str] # expected endpoints for community B
73
- proposed_at: int # unix seconds
74
- proposer_sig: str # Ed25519 sig over the proposal body by an anchor of A
75
 
76
 
77
  # ---------------------------------------------------------------------------
 
1
  """Federation manifest builder and verifier (M14)."""
2
+
3
  from __future__ import annotations
4
 
5
  import base64
 
45
  community_a_name: str
46
  community_b_id: str
47
  community_b_name: str
48
+ scope_a_to_b: FederationScope # what A grants B
49
+ scope_b_to_a: FederationScope # what B grants A
50
+ sig_a: str # Ed25519 sig from anchor of community A
51
+ sig_b: str # Ed25519 sig from anchor of community B
52
+ co_signers_a: list[str] # additional anchor signatures from community A
53
+ co_signers_b: list[str] # additional anchor signatures from community B
54
+ created_at: int # unix seconds
55
+ expires_at: int # unix seconds
56
  bootstrap_endpoints_a: list[str]
57
  bootstrap_endpoints_b: list[str]
58
 
 
65
  class FederationProposal:
66
  """A draft federation proposal from community A to community B."""
67
 
68
+ community_a: str # community_id of proposer
69
+ community_b: str # community_id of target
70
+ scope_a: FederationScope # scope A proposes to grant B
71
+ scope_b: FederationScope # scope A requests from B
72
+ bootstrap_a: list[str] # endpoints for community A
73
+ bootstrap_b: list[str] # expected endpoints for community B
74
+ proposed_at: int # unix seconds
75
+ proposer_sig: str # Ed25519 sig over the proposal body by an anchor of A
76
 
77
 
78
  # ---------------------------------------------------------------------------
hearthnet/federation/peering.py CHANGED
@@ -1,4 +1,5 @@
1
  """Cross-community peering store and HTTP client (M14)."""
 
2
  from __future__ import annotations
3
 
4
  import json
 
1
  """Cross-community peering store and HTTP client (M14)."""
2
+
3
  from __future__ import annotations
4
 
5
  import json
hearthnet/federation/service.py CHANGED
@@ -1,4 +1,5 @@
1
  """FederationService β€” registers federation.* capabilities on the bus (M14)."""
 
2
  from __future__ import annotations
3
 
4
  from typing import Any
@@ -55,10 +56,11 @@ class FederationService:
55
  ("federation.peer.add", "1.0", self._handle_add),
56
  ("federation.peer.remove", "1.0", self._handle_remove),
57
  ]
58
- for name, version, handler in descriptors:
 
59
  desc = CapabilityDescriptor(
60
  name=name,
61
- version=version,
62
  stability="stable",
63
  params={},
64
  max_concurrent=2,
@@ -87,16 +89,18 @@ class FederationService:
87
  peer_id = m.community_a_id
88
  peer_name = m.community_a_name
89
  scope = m.scope_a_to_b
90
- peers.append({
91
- "community_id": peer_id,
92
- "community_name": peer_name,
93
- "federation_id": m.federation_id,
94
- "scope": {
95
- "capabilities": list(scope.capabilities),
96
- "data_visibility": scope.data_visibility,
97
- },
98
- "expires_at": m.expires_at,
99
- })
 
 
100
  return {"peers": peers}
101
 
102
  def _handle_add(self, params: dict) -> dict:
 
1
  """FederationService β€” registers federation.* capabilities on the bus (M14)."""
2
+
3
  from __future__ import annotations
4
 
5
  from typing import Any
 
56
  ("federation.peer.add", "1.0", self._handle_add),
57
  ("federation.peer.remove", "1.0", self._handle_remove),
58
  ]
59
+ for name, version_str, handler in descriptors:
60
+ major, minor = map(int, version_str.split("."))
61
  desc = CapabilityDescriptor(
62
  name=name,
63
+ version=(major, minor),
64
  stability="stable",
65
  params={},
66
  max_concurrent=2,
 
89
  peer_id = m.community_a_id
90
  peer_name = m.community_a_name
91
  scope = m.scope_a_to_b
92
+ peers.append(
93
+ {
94
+ "community_id": peer_id,
95
+ "community_name": peer_name,
96
+ "federation_id": m.federation_id,
97
+ "scope": {
98
+ "capabilities": list(scope.capabilities),
99
+ "data_visibility": scope.data_visibility,
100
+ },
101
+ "expires_at": m.expires_at,
102
+ }
103
+ )
104
  return {"peers": peers}
105
 
106
  def _handle_add(self, params: dict) -> dict:
hearthnet/fedlearn/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
  """M28 β€” Federated Learning package (experimental, Phase 3)."""
 
2
  from __future__ import annotations
3
 
4
- from hearthnet.fedlearn.coordinator import FedLearnCoordinator, RoundManifest, ParticipantSubmission
5
 
6
- __all__ = ["FedLearnCoordinator", "RoundManifest", "ParticipantSubmission"]
 
1
  """M28 β€” Federated Learning package (experimental, Phase 3)."""
2
+
3
  from __future__ import annotations
4
 
5
+ from hearthnet.fedlearn.coordinator import FedLearnCoordinator, ParticipantSubmission, RoundManifest
6
 
7
+ __all__ = ["FedLearnCoordinator", "ParticipantSubmission", "RoundManifest"]
hearthnet/fedlearn/coordinator.py CHANGED
@@ -4,6 +4,7 @@ FedAvg on LoRA adapter weight deltas. Each node trains locally;
4
  only adapter deltas (not raw data or full weights) are shared.
5
  Gated by config.research.federated_learning = True.
6
  """
 
7
  from __future__ import annotations
8
 
9
  import time
@@ -17,6 +18,7 @@ RoundID = NewType("RoundID", str)
17
  @dataclass(frozen=True)
18
  class RoundManifest:
19
  """Describes a federated learning round."""
 
20
  round_id: RoundID
21
  base_model_id: str
22
  coordinator_node_id: str
@@ -35,7 +37,7 @@ class RoundManifest:
35
  class ParticipantSubmission:
36
  round_id: RoundID
37
  participant_node_id: str
38
- delta_bytes: bytes # serialised LoRA state dict subset (safetensors format)
39
  num_samples: int
40
  submitted_at: float = field(default_factory=time.time)
41
  participant_sig: bytes = b""
 
4
  only adapter deltas (not raw data or full weights) are shared.
5
  Gated by config.research.federated_learning = True.
6
  """
7
+
8
  from __future__ import annotations
9
 
10
  import time
 
18
  @dataclass(frozen=True)
19
  class RoundManifest:
20
  """Describes a federated learning round."""
21
+
22
  round_id: RoundID
23
  base_model_id: str
24
  coordinator_node_id: str
 
37
  class ParticipantSubmission:
38
  round_id: RoundID
39
  participant_node_id: str
40
+ delta_bytes: bytes # serialised LoRA state dict subset (safetensors format)
41
  num_samples: int
42
  submitted_at: float = field(default_factory=time.time)
43
  participant_sig: bytes = b""
hearthnet/identity/keys.py CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ"""M01 - Node identity: Ed25519 key management.
2
 
3
  Spec: docs/M01-identity.md Β§3.1
4
  Impl-ref: impl_ref.md Β§5
@@ -6,6 +6,7 @@ Impl-ref: impl_ref.md Β§5
6
  Keys stored in keys_dir (default ~/.hearthnet/keys/).
7
  Sign/verify via PyNaCl Ed25519. canonical_json() for deterministic signing.
8
  """
 
9
  from __future__ import annotations
10
 
11
  import base64
@@ -70,16 +71,15 @@ def full_node_id(verify_key_bytes: bytes) -> str:
70
  def parse_node_id(node_id: str) -> bytes:
71
  """Decode a full node_id to 32 bytes. Short form raises ValueError."""
72
  import re
 
73
  if not node_id.startswith("ed25519:"):
74
  raise ValueError(f"node_id must start with 'ed25519:': {node_id!r}")
75
- payload = node_id[len("ed25519:"):]
76
  # Short form is b32-with-dashes: groups of [A-Z2-7=]{1,4} separated by '-'
77
  # e.g. "SQ2J-OH7E-LCMU-Y===" Ò€” always shorter than 30 chars and matches this pattern.
78
  # Full form is 43-char base64url (no '=' padding).
79
  if re.fullmatch(r"[A-Z2-7=]{1,4}(-[A-Z2-7=]{1,4}){1,}", payload):
80
- raise ValueError(
81
- "Short node IDs cannot be decoded to raw bytes; use full form."
82
- )
83
  # Add padding back for base64url decoding
84
  padded = payload + "=" * (4 - len(payload) % 4 if len(payload) % 4 != 0 else 0)
85
  raw = base64.urlsafe_b64decode(padded)
@@ -181,7 +181,7 @@ def verify_payload(payload: dict, vk: Any) -> bool: # vk: nacl.signing.VerifyKe
181
  raw_sig = payload.get("signature", "")
182
  if not raw_sig.startswith("ed25519:"):
183
  raise IdentityError("verify_failed", reason="signature field missing or malformed")
184
- sig_b64 = raw_sig[len("ed25519:"):]
185
  padding = 4 - len(sig_b64) % 4
186
  if padding != 4:
187
  sig_b64 += "=" * padding
@@ -243,9 +243,7 @@ def save(kp: KeyPair, keys_dir: Path) -> None:
243
  pub_path = keys_dir / "device.pub"
244
  # Write private key (raw 32-byte seed, base64url encoded)
245
  sk_bytes = bytes(kp.signing_key)
246
- priv_path.write_bytes(
247
- base64.urlsafe_b64encode(sk_bytes).rstrip(b"=") + b"\n"
248
- )
249
  # Restrict permissions on POSIX
250
  try:
251
  os.chmod(priv_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
@@ -253,9 +251,7 @@ def save(kp: KeyPair, keys_dir: Path) -> None:
253
  pass # Windows: chmod semantics differ; best-effort
254
  # Write public key
255
  vk_bytes = bytes(kp.verify_key)
256
- pub_path.write_bytes(
257
- base64.urlsafe_b64encode(vk_bytes).rstrip(b"=") + b"\n"
258
- )
259
 
260
 
261
  def load(keys_dir: Path) -> KeyPair:
@@ -305,4 +301,3 @@ def load_or_generate(keys_dir: Path) -> KeyPair:
305
  kp = generate()
306
  save(kp, keys_dir)
307
  return kp
308
-
 
1
+ """M01 - Node identity: Ed25519 key management.
2
 
3
  Spec: docs/M01-identity.md Β§3.1
4
  Impl-ref: impl_ref.md Β§5
 
6
  Keys stored in keys_dir (default ~/.hearthnet/keys/).
7
  Sign/verify via PyNaCl Ed25519. canonical_json() for deterministic signing.
8
  """
9
+
10
  from __future__ import annotations
11
 
12
  import base64
 
71
  def parse_node_id(node_id: str) -> bytes:
72
  """Decode a full node_id to 32 bytes. Short form raises ValueError."""
73
  import re
74
+
75
  if not node_id.startswith("ed25519:"):
76
  raise ValueError(f"node_id must start with 'ed25519:': {node_id!r}")
77
+ payload = node_id[len("ed25519:") :]
78
  # Short form is b32-with-dashes: groups of [A-Z2-7=]{1,4} separated by '-'
79
  # e.g. "SQ2J-OH7E-LCMU-Y===" Ò€” always shorter than 30 chars and matches this pattern.
80
  # Full form is 43-char base64url (no '=' padding).
81
  if re.fullmatch(r"[A-Z2-7=]{1,4}(-[A-Z2-7=]{1,4}){1,}", payload):
82
+ raise ValueError("Short node IDs cannot be decoded to raw bytes; use full form.")
 
 
83
  # Add padding back for base64url decoding
84
  padded = payload + "=" * (4 - len(payload) % 4 if len(payload) % 4 != 0 else 0)
85
  raw = base64.urlsafe_b64decode(padded)
 
181
  raw_sig = payload.get("signature", "")
182
  if not raw_sig.startswith("ed25519:"):
183
  raise IdentityError("verify_failed", reason="signature field missing or malformed")
184
+ sig_b64 = raw_sig[len("ed25519:") :]
185
  padding = 4 - len(sig_b64) % 4
186
  if padding != 4:
187
  sig_b64 += "=" * padding
 
243
  pub_path = keys_dir / "device.pub"
244
  # Write private key (raw 32-byte seed, base64url encoded)
245
  sk_bytes = bytes(kp.signing_key)
246
+ priv_path.write_bytes(base64.urlsafe_b64encode(sk_bytes).rstrip(b"=") + b"\n")
 
 
247
  # Restrict permissions on POSIX
248
  try:
249
  os.chmod(priv_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
 
251
  pass # Windows: chmod semantics differ; best-effort
252
  # Write public key
253
  vk_bytes = bytes(kp.verify_key)
254
+ pub_path.write_bytes(base64.urlsafe_b64encode(vk_bytes).rstrip(b"=") + b"\n")
 
 
255
 
256
 
257
  def load(keys_dir: Path) -> KeyPair:
 
301
  kp = generate()
302
  save(kp, keys_dir)
303
  return kp
 
hearthnet/identity/manifest.py CHANGED
@@ -1,8 +1,8 @@
1
  from __future__ import annotations
2
 
3
  from dataclasses import dataclass
4
- from datetime import datetime, timedelta, timezone
5
- from typing import Any, Optional
6
 
7
  from hearthnet.identity.keys import (
8
  IdentityError,
@@ -70,14 +70,30 @@ _NODE_MANIFEST_TTL_SECONDS = 30
70
  _COMMUNITY_MANIFEST_TTL_SECONDS = 86400
71
 
72
  _REQUIRED_NODE_FIELDS = {
73
- "version", "node_id", "display_name", "community_id", "profile",
74
- "endpoints", "capabilities", "issued_at", "expires_at", "contract_version",
 
 
 
 
 
 
 
 
75
  "signature",
76
  }
77
 
78
  _REQUIRED_COMMUNITY_FIELDS = {
79
- "version", "community_id", "name", "root_node_id", "members", "policy",
80
- "issued_at", "expires_at", "contract_version", "signature",
 
 
 
 
 
 
 
 
81
  }
82
 
83
 
@@ -87,11 +103,11 @@ def _parse_rfc3339(s: str) -> datetime:
87
  s = s.rstrip("Z")
88
  if "+" in s:
89
  s = s[: s.index("+")]
90
- return datetime.fromisoformat(s).replace(tzinfo=timezone.utc)
91
 
92
 
93
  def _now_utc() -> datetime:
94
- return datetime.now(timezone.utc)
95
 
96
 
97
  def _rfc3339(dt: datetime) -> str:
@@ -129,8 +145,7 @@ class NodeManifest:
129
  "community_id": self.community_id,
130
  "profile": self.profile,
131
  "endpoints": [
132
- {"transport": e.transport, "host": e.host, "port": e.port}
133
- for e in self.endpoints
134
  ],
135
  "capabilities": [
136
  {
@@ -217,8 +232,7 @@ def build_node_manifest(
217
  "community_id": community_id,
218
  "profile": profile,
219
  "endpoints": [
220
- {"transport": e.transport, "host": e.host, "port": e.port}
221
- for e in endpoints
222
  ],
223
  "capabilities": [
224
  {
 
1
  from __future__ import annotations
2
 
3
  from dataclasses import dataclass
4
+ from datetime import UTC, datetime, timedelta
5
+ from typing import Any
6
 
7
  from hearthnet.identity.keys import (
8
  IdentityError,
 
70
  _COMMUNITY_MANIFEST_TTL_SECONDS = 86400
71
 
72
  _REQUIRED_NODE_FIELDS = {
73
+ "version",
74
+ "node_id",
75
+ "display_name",
76
+ "community_id",
77
+ "profile",
78
+ "endpoints",
79
+ "capabilities",
80
+ "issued_at",
81
+ "expires_at",
82
+ "contract_version",
83
  "signature",
84
  }
85
 
86
  _REQUIRED_COMMUNITY_FIELDS = {
87
+ "version",
88
+ "community_id",
89
+ "name",
90
+ "root_node_id",
91
+ "members",
92
+ "policy",
93
+ "issued_at",
94
+ "expires_at",
95
+ "contract_version",
96
+ "signature",
97
  }
98
 
99
 
 
103
  s = s.rstrip("Z")
104
  if "+" in s:
105
  s = s[: s.index("+")]
106
+ return datetime.fromisoformat(s).replace(tzinfo=UTC)
107
 
108
 
109
  def _now_utc() -> datetime:
110
+ return datetime.now(UTC)
111
 
112
 
113
  def _rfc3339(dt: datetime) -> str:
 
145
  "community_id": self.community_id,
146
  "profile": self.profile,
147
  "endpoints": [
148
+ {"transport": e.transport, "host": e.host, "port": e.port} for e in self.endpoints
 
149
  ],
150
  "capabilities": [
151
  {
 
232
  "community_id": community_id,
233
  "profile": profile,
234
  "endpoints": [
235
+ {"transport": e.transport, "host": e.host, "port": e.port} for e in endpoints
 
236
  ],
237
  "capabilities": [
238
  {
hearthnet/identity/tokens.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  Token format: hntoken://v1/<b64url(header)>.<b64url(payload)>.<b64url(sig)>
4
  """
 
5
  from __future__ import annotations
6
 
7
  import base64
@@ -40,14 +41,14 @@ class TokenScope:
40
  class CapabilityToken:
41
  """A signed Ed25519 capability token."""
42
 
43
- iss: str # issuer node_id (full form "ed25519:…")
44
- sub: str # subject node_id or "*" for bearer token
45
- aud: str # audience community_id or ""
46
- iat: int # issued-at unix seconds
47
- exp: int # expires-at unix seconds
48
- nbf: int # not-before unix seconds
49
  scope: TokenScope
50
- jti: str # unique token ID (ULID)
51
  issued_via: str # "federation"|"onboarding"|"manual"|"relay"
52
 
53
 
@@ -170,7 +171,7 @@ def decode_token(text: str) -> CapabilityToken:
170
  """Parse an hntoken:// string. Validates structure; does NOT verify the signature."""
171
  if not text.startswith(_TOKEN_SCHEME):
172
  raise TokenError(f"Not a HearthNet token (expected 'hntoken://v1/'): {text[:40]!r}")
173
- body = text[len(_TOKEN_SCHEME):]
174
  parts = body.split(".")
175
  if len(parts) != 3:
176
  raise TokenError("Token must have exactly 3 dot-separated parts")
 
2
 
3
  Token format: hntoken://v1/<b64url(header)>.<b64url(payload)>.<b64url(sig)>
4
  """
5
+
6
  from __future__ import annotations
7
 
8
  import base64
 
41
  class CapabilityToken:
42
  """A signed Ed25519 capability token."""
43
 
44
+ iss: str # issuer node_id (full form "ed25519:…")
45
+ sub: str # subject node_id or "*" for bearer token
46
+ aud: str # audience community_id or ""
47
+ iat: int # issued-at unix seconds
48
+ exp: int # expires-at unix seconds
49
+ nbf: int # not-before unix seconds
50
  scope: TokenScope
51
+ jti: str # unique token ID (ULID)
52
  issued_via: str # "federation"|"onboarding"|"manual"|"relay"
53
 
54
 
 
171
  """Parse an hntoken:// string. Validates structure; does NOT verify the signature."""
172
  if not text.startswith(_TOKEN_SCHEME):
173
  raise TokenError(f"Not a HearthNet token (expected 'hntoken://v1/'): {text[:40]!r}")
174
+ body = text[len(_TOKEN_SCHEME) :]
175
  parts = body.split(".")
176
  if len(parts) != 3:
177
  raise TokenError("Token must have exactly 3 dot-separated parts")
hearthnet/lora/__init__.py CHANGED
@@ -1,6 +1,12 @@
1
  """LoRa hardware beacons package (experimental, Phase 3 β€” M29)."""
 
2
  from __future__ import annotations
3
 
4
- from hearthnet.lora.service import LoraBeacon, LoraBeaconService, decode_beacon_frame, encode_beacon_frame
 
 
 
 
 
5
 
6
  __all__ = ["LoraBeacon", "LoraBeaconService", "decode_beacon_frame", "encode_beacon_frame"]
 
1
  """LoRa hardware beacons package (experimental, Phase 3 β€” M29)."""
2
+
3
  from __future__ import annotations
4
 
5
+ from hearthnet.lora.service import (
6
+ LoraBeacon,
7
+ LoraBeaconService,
8
+ decode_beacon_frame,
9
+ encode_beacon_frame,
10
+ )
11
 
12
  __all__ = ["LoraBeacon", "LoraBeaconService", "decode_beacon_frame", "encode_beacon_frame"]
hearthnet/lora/service.py CHANGED
@@ -4,6 +4,7 @@
4
  No AI traffic, no chat, no file transfer β€” only 32-byte heartbeat frames.
5
  Gated by config.research.lora_beacons = True.
6
  """
 
7
  from __future__ import annotations
8
 
9
  import struct
@@ -28,10 +29,10 @@ FRAME_SIZE = 32
28
  class LoraBeacon:
29
  beacon_id: LoraBeaconID
30
  device_id: LoraDeviceID
31
- node_id_hash: bytes # 8 bytes
32
  sequence: int
33
- flags: int # bit0=emergency, bit1=panic
34
- rssi: int | None = None # dBm, if available
35
  received_at: float = field(default_factory=time.time)
36
 
37
  @property
@@ -46,6 +47,7 @@ class LoraBeacon:
46
  def encode_beacon_frame(node_id_full: str, sequence: int, flags: int = 0) -> bytes:
47
  """Encode a 32-byte LoRa beacon frame."""
48
  import hashlib
 
49
  node_hash = hashlib.sha256(node_id_full.encode()).digest()[:8]
50
  header = struct.pack(">4sI8sB", FRAME_MAGIC, sequence, node_hash, flags)
51
  return header + b"\x00" * (FRAME_SIZE - len(header))
@@ -94,6 +96,7 @@ class LoraBeaconService:
94
  """Write frame to serial LoRa hardware (stub β€” real impl needs pyserial)."""
95
  try:
96
  import serial # type: ignore[import-untyped]
 
97
  with serial.Serial(self._serial_port, baudrate=9600, timeout=1) as ser:
98
  ser.write(frame)
99
  except ImportError:
 
4
  No AI traffic, no chat, no file transfer β€” only 32-byte heartbeat frames.
5
  Gated by config.research.lora_beacons = True.
6
  """
7
+
8
  from __future__ import annotations
9
 
10
  import struct
 
29
  class LoraBeacon:
30
  beacon_id: LoraBeaconID
31
  device_id: LoraDeviceID
32
+ node_id_hash: bytes # 8 bytes
33
  sequence: int
34
+ flags: int # bit0=emergency, bit1=panic
35
+ rssi: int | None = None # dBm, if available
36
  received_at: float = field(default_factory=time.time)
37
 
38
  @property
 
47
  def encode_beacon_frame(node_id_full: str, sequence: int, flags: int = 0) -> bytes:
48
  """Encode a 32-byte LoRa beacon frame."""
49
  import hashlib
50
+
51
  node_hash = hashlib.sha256(node_id_full.encode()).digest()[:8]
52
  header = struct.pack(">4sI8sB", FRAME_MAGIC, sequence, node_hash, flags)
53
  return header + b"\x00" * (FRAME_SIZE - len(header))
 
96
  """Write frame to serial LoRa hardware (stub β€” real impl needs pyserial)."""
97
  try:
98
  import serial # type: ignore[import-untyped]
99
+
100
  with serial.Serial(self._serial_port, baudrate=9600, timeout=1) as ser:
101
  ser.write(frame)
102
  except ImportError:
hearthnet/mobile/invite.py CHANGED
@@ -4,6 +4,7 @@ Generates mobile-targeted invite deep links (``hnapp://``) and QR codes
4
  for the mobile native client (Flutter). Builds on top of the Phase 1
5
  onboarding module (M13).
6
  """
 
7
  from __future__ import annotations
8
 
9
  import base64
@@ -11,8 +12,6 @@ import hashlib
11
  import json
12
  import time
13
  from dataclasses import dataclass, field
14
- from typing import Any
15
-
16
 
17
  # ---------------------------------------------------------------------------
18
  # Invite blob for mobile clients
@@ -77,11 +76,11 @@ class MobileInviteBlob:
77
  return hashlib.sha256(raw.encode()).hexdigest()[:16]
78
 
79
  @classmethod
80
- def from_deep_link(cls, deep_link: str) -> "MobileInviteBlob":
81
  """Parse a deep link produced by :meth:`to_deep_link`."""
82
  if not deep_link.startswith("hnapp://v1/"):
83
  raise ValueError(f"Not a valid hnapp:// deep link: {deep_link!r}")
84
- b64 = deep_link[len("hnapp://v1/"):]
85
  # Re-add padding
86
  padding = 4 - len(b64) % 4
87
  if padding != 4:
 
4
  for the mobile native client (Flutter). Builds on top of the Phase 1
5
  onboarding module (M13).
6
  """
7
+
8
  from __future__ import annotations
9
 
10
  import base64
 
12
  import json
13
  import time
14
  from dataclasses import dataclass, field
 
 
15
 
16
  # ---------------------------------------------------------------------------
17
  # Invite blob for mobile clients
 
76
  return hashlib.sha256(raw.encode()).hexdigest()[:16]
77
 
78
  @classmethod
79
+ def from_deep_link(cls, deep_link: str) -> MobileInviteBlob:
80
  """Parse a deep link produced by :meth:`to_deep_link`."""
81
  if not deep_link.startswith("hnapp://v1/"):
82
  raise ValueError(f"Not a valid hnapp:// deep link: {deep_link!r}")
83
+ b64 = deep_link[len("hnapp://v1/") :]
84
  # Re-add padding
85
  padding = 4 - len(b64) % 4
86
  if padding != 4:
hearthnet/mobile/push_authority.py CHANGED
@@ -8,12 +8,13 @@ The anchor-side service that mobile clients (Flutter) call to:
8
  Push delivery itself is *out of scope* for the local-first anchor β€” if the
9
  relay tier (M15) is configured the anchor forwards the notification there.
10
  """
 
11
  from __future__ import annotations
12
 
13
  import hashlib
14
  import time
15
  from dataclasses import dataclass, field
16
- from typing import Any, TYPE_CHECKING
17
 
18
  if TYPE_CHECKING: # pragma: no cover
19
  from hearthnet.bus.router import Router
@@ -122,7 +123,7 @@ class MobilePushService:
122
  def __init__(
123
  self,
124
  relay_url: str | None = None,
125
- bus: "Router | None" = None,
126
  ) -> None:
127
  self._registry = PushTokenRegistry()
128
  self._relay_url = relay_url
@@ -160,7 +161,10 @@ class MobilePushService:
160
  token = str(inp.get("token", ""))
161
  platform = str(inp.get("platform", "unknown"))
162
  if not node_id or not token:
163
- return {"output": {"error": "bad_request", "detail": "node_id and token required"}, "meta": {}}
 
 
 
164
  entry = self._registry.register(node_id, token, platform)
165
  return {"output": {"status": "registered", "token_hash": entry.token_hash}, "meta": {}}
166
 
 
8
  Push delivery itself is *out of scope* for the local-first anchor β€” if the
9
  relay tier (M15) is configured the anchor forwards the notification there.
10
  """
11
+
12
  from __future__ import annotations
13
 
14
  import hashlib
15
  import time
16
  from dataclasses import dataclass, field
17
+ from typing import TYPE_CHECKING
18
 
19
  if TYPE_CHECKING: # pragma: no cover
20
  from hearthnet.bus.router import Router
 
123
  def __init__(
124
  self,
125
  relay_url: str | None = None,
126
+ bus: Router | None = None,
127
  ) -> None:
128
  self._registry = PushTokenRegistry()
129
  self._relay_url = relay_url
 
161
  token = str(inp.get("token", ""))
162
  platform = str(inp.get("platform", "unknown"))
163
  if not node_id or not token:
164
+ return {
165
+ "output": {"error": "bad_request", "detail": "node_id and token required"},
166
+ "meta": {},
167
+ }
168
  entry = self._registry.register(node_id, token, platform)
169
  return {"output": {"status": "registered", "token_hash": entry.token_hash}, "meta": {}}
170
 
hearthnet/moe/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
  """M27 β€” MoE Expert Routing package (experimental, Phase 3)."""
 
2
  from __future__ import annotations
3
 
4
  from hearthnet.moe.router import ExpertDescriptor, ExpertRegistry, MoeRouter, RouteResult
 
1
  """M27 β€” MoE Expert Routing package (experimental, Phase 3)."""
2
+
3
  from __future__ import annotations
4
 
5
  from hearthnet.moe.router import ExpertDescriptor, ExpertRegistry, MoeRouter, RouteResult