Spaces:
Running on Zero
fix: 0 test failures; FileService; real RagService; emergency probe; chat return
Browse filesTESTS:
- 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
- QUALITY_REPORT.md +260 -0
- app.py +88 -31
- docs/sample.txt +190 -0
- docs/screenshots/node-a-ask-tab.png +0 -0
- docs/screenshots/node-b-settings-tab.png +0 -0
- hearthnet/blobs/chunker.py +0 -1
- hearthnet/blobs/transfer.py +3 -1
- hearthnet/bus/router.py +2 -2
- hearthnet/bus/schema.py +3 -6
- hearthnet/bus/trace.py +1 -0
- hearthnet/civdef/__init__.py +1 -0
- hearthnet/civdef/service.py +25 -19
- hearthnet/cli.py +7 -2
- hearthnet/config.py +37 -16
- hearthnet/constants.py +5 -9
- hearthnet/crypto/envelope.py +4 -7
- hearthnet/crypto/kem.py +2 -3
- hearthnet/crypto/prekeys.py +1 -0
- hearthnet/crypto/ratchet.py +1 -0
- hearthnet/dht/bootstrap.py +2 -2
- hearthnet/dht/kademlia.py +9 -10
- hearthnet/discovery/__init__.py +7 -3
- hearthnet/discovery/mdns.py +4 -0
- hearthnet/discovery/peers.py +13 -5
- hearthnet/discovery/udp.py +11 -7
- hearthnet/distributed_inference/__init__.py +3 -2
- hearthnet/distributed_inference/pipeline.py +4 -1
- hearthnet/distributed_inference/shard.py +6 -6
- hearthnet/emergency/detector.py +7 -6
- hearthnet/emergency/state.py +2 -1
- hearthnet/events/__init__.py +7 -7
- hearthnet/events/log.py +9 -5
- hearthnet/events/snapshot.py +10 -6
- hearthnet/events/sync.py +1 -3
- hearthnet/events/types.py +5 -5
- hearthnet/evidence/__init__.py +3 -2
- hearthnet/evidence/store.py +12 -7
- hearthnet/federation/manifest.py +17 -16
- hearthnet/federation/peering.py +1 -0
- hearthnet/federation/service.py +16 -12
- hearthnet/fedlearn/__init__.py +3 -2
- hearthnet/fedlearn/coordinator.py +3 -1
- hearthnet/identity/keys.py +8 -13
- hearthnet/identity/manifest.py +26 -12
- hearthnet/identity/tokens.py +9 -8
- hearthnet/lora/__init__.py +7 -1
- hearthnet/lora/service.py +6 -3
- hearthnet/mobile/invite.py +3 -4
- hearthnet/mobile/push_authority.py +7 -3
- hearthnet/moe/__init__.py +1 -0
|
@@ -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*
|
|
@@ -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
|
| 66 |
-
"
|
| 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
|
| 82 |
"text": (
|
| 83 |
-
"
|
| 84 |
-
"
|
| 85 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
),
|
| 87 |
},
|
| 88 |
{
|
| 89 |
"id": "setup.001",
|
| 90 |
-
"title": "Node Setup",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
"text": (
|
| 92 |
-
"
|
| 93 |
-
"
|
| 94 |
-
"
|
| 95 |
-
"for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
|
|
@@ -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
|
|
|
|
|
|
@@ -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 |
|
|
@@ -51,7 +51,9 @@ class TransferManager:
|
|
| 51 |
resp.raise_for_status()
|
| 52 |
raw = resp.json()
|
| 53 |
except Exception as exc:
|
| 54 |
-
raise BlobError(
|
|
|
|
|
|
|
| 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 |
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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
|
|
|
|
@@ -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 |
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
| 34 |
-
ALERT
|
| 35 |
-
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
|
| 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
|
| 66 |
title: str
|
| 67 |
body: str
|
| 68 |
-
area_description: str
|
| 69 |
issuer_node_id: str
|
| 70 |
issuer_role_cert_id: str | None
|
| 71 |
community_id: str
|
| 72 |
-
event_log_id: str | None = None
|
| 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(
|
| 172 |
-
"
|
| 173 |
-
|
| 174 |
-
|
| 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(
|
| 188 |
-
"
|
| 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)
|
|
@@ -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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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."""
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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(
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 307 |
-
|
|
|
|
| 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(
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 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(
|
| 406 |
-
|
| 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
|
|
|
|
@@ -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
|
| 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
|
| 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
|
| 78 |
-
MARKET_MAX_TTL_SECONDS: int = 86400 * 30
|
| 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
|
|
|
|
|
|
|
|
|
|
@@ -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
|
| 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))
|
|
@@ -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 |
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
| 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,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
|
| 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():
|
|
@@ -2,23 +2,22 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import hashlib
|
| 4 |
import time
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
from typing import Any
|
| 7 |
|
| 8 |
|
| 9 |
@dataclass(frozen=True)
|
| 10 |
class DhtContact:
|
| 11 |
-
node_key: bytes
|
| 12 |
-
endpoint: str
|
| 13 |
-
node_id: str
|
| 14 |
-
last_seen: float
|
| 15 |
|
| 16 |
|
| 17 |
@dataclass(frozen=True)
|
| 18 |
class DhtValue:
|
| 19 |
-
key: bytes
|
| 20 |
-
payload: dict
|
| 21 |
-
expires_at: int
|
| 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 |
|
|
@@ -3,7 +3,11 @@ from hearthnet.discovery.peers import PeerEvent, PeerRecord, PeerRegistry
|
|
| 3 |
from hearthnet.discovery.udp import UdpAnnouncer, UdpListener
|
| 4 |
|
| 5 |
__all__ = [
|
| 6 |
-
"
|
| 7 |
-
"
|
| 8 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
]
|
|
@@ -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(
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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
|
| 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
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
@@ -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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
| 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)
|
|
@@ -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__ = ["
|
|
|
|
| 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"]
|
|
@@ -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"
|
| 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
|
|
@@ -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 |
-
|
|
|
|
| 16 |
model_id: str
|
| 17 |
layer_lo: int
|
| 18 |
-
layer_hi: int
|
| 19 |
node_id: str
|
| 20 |
endpoint: str
|
| 21 |
-
dtype: str = "float16"
|
| 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
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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(
|
| 91 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import time
|
|
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
-
from typing import
|
| 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 |
|
|
@@ -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 |
-
"
|
| 19 |
"Snapshot",
|
| 20 |
-
"
|
| 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 |
]
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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
|
| 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(
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
@@ -4,7 +4,7 @@ import base64
|
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
from dataclasses import dataclass
|
| 7 |
-
from datetime import
|
| 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(
|
| 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:
|
| 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 |
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:
|
| 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]
|
| 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
|
|
@@ -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,
|
|
@@ -46,12 +46,12 @@ def new_ulid() -> str:
|
|
| 46 |
|
| 47 |
@dataclass(frozen=True)
|
| 48 |
class Event:
|
| 49 |
-
schema_version: int
|
| 50 |
-
event_id: str
|
| 51 |
event_type: EventType
|
| 52 |
community_id: str
|
| 53 |
-
author: str
|
| 54 |
lamport: int
|
| 55 |
payload: dict[str, Any]
|
| 56 |
-
issued_at: str
|
| 57 |
-
signature: 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 ""
|
|
@@ -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,
|
| 5 |
|
| 6 |
-
__all__ = ["Claim", "ClaimID", "ClaimSource", "ClaimStore", "
|
|
|
|
| 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"]
|
|
@@ -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
|
| 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
|
| 34 |
-
predicate: str
|
| 35 |
-
object_: str
|
| 36 |
-
asserted_by: str
|
| 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(
|
|
|
|
|
|
|
| 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.
|
|
@@ -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
|
| 48 |
-
scope_b_to_a: FederationScope
|
| 49 |
-
sig_a: str
|
| 50 |
-
sig_b: str
|
| 51 |
-
co_signers_a: list[str]
|
| 52 |
-
co_signers_b: list[str]
|
| 53 |
-
created_at: int
|
| 54 |
-
expires_at: int
|
| 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
|
| 68 |
-
community_b: str
|
| 69 |
-
scope_a: FederationScope
|
| 70 |
-
scope_b: FederationScope
|
| 71 |
-
bootstrap_a: list[str]
|
| 72 |
-
bootstrap_b: list[str]
|
| 73 |
-
proposed_at: int
|
| 74 |
-
proposer_sig: str
|
| 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 |
# ---------------------------------------------------------------------------
|
|
@@ -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
|
|
@@ -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,
|
|
|
|
| 59 |
desc = CapabilityDescriptor(
|
| 60 |
name=name,
|
| 61 |
-
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 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
"
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 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:
|
|
@@ -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,
|
| 5 |
|
| 6 |
-
__all__ = ["FedLearnCoordinator", "
|
|
|
|
| 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"]
|
|
@@ -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
|
| 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""
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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
|
|
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from datetime import
|
| 5 |
-
from typing import Any
|
| 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",
|
| 74 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
"signature",
|
| 76 |
}
|
| 77 |
|
| 78 |
_REQUIRED_COMMUNITY_FIELDS = {
|
| 79 |
-
"version",
|
| 80 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 91 |
|
| 92 |
|
| 93 |
def _now_utc() -> datetime:
|
| 94 |
-
return datetime.now(
|
| 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 |
{
|
|
@@ -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
|
| 44 |
-
sub: str
|
| 45 |
-
aud: str
|
| 46 |
-
iat: int
|
| 47 |
-
exp: int
|
| 48 |
-
nbf: int
|
| 49 |
scope: TokenScope
|
| 50 |
-
jti: str
|
| 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")
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"]
|
|
@@ -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
|
| 32 |
sequence: int
|
| 33 |
-
flags: int
|
| 34 |
-
rssi: int | None = None
|
| 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:
|
|
@@ -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) ->
|
| 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:
|
|
@@ -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
|
| 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:
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -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
|