# HearthNet — Implementation Reference **Purpose:** complete inventory of every file, class, method, and function across the spec set. Use this as a coding checklist. For *why* and behaviour → follow the spec link on each entry. For *what to call it* and *what it returns* → this document is normative. **Spec set:** see [`00-OVERVIEW.md`](00-OVERVIEW.md), [`GLOSSARY.md`](GLOSSARY.md), [`CAPABILITY_CONTRACT.md`](CAPABILITY_CONTRACT.md). --- ## 0. Conventions (read first) ### 0.1 Type aliases — `hearthnet/types.py` *Re-exported by every module that uses them. Never invent synonyms.* — [00-OVERVIEW §4.1](00-OVERVIEW.md) | Alias | Underlying | Example | |-------|------------|---------| | `NodeID` | `str` | `"ed25519:7H4G-Y9KL-2P3M-X8QR"` (short) or full base64-url | | `CommunityID` | `str` | full base64-url | | `CapabilityName` | `str` | `"llm.chat"` | | `Version` | `tuple[int, int]` | `(1, 0)` | | `Lamport` | `int` | monotonic per community | | `CID` | `str` | `"blake3:"` | | `EventID` | `str` | ULID | | `TraceID` | `str` | ULID | | `SchemaHash` | `str` | `"blake3:"` | | `WallClock` | `str` | `"2026-05-26T08:14:22Z"` | | `Signature` | `str` | `"ed25519:"` | | `Topic` | `str` | `"marketplace.post.created"` | | `ErrorCode` | `Literal[...]` | see [CONTRACT §9](CAPABILITY_CONTRACT.md) | | `TrustLevel` | `Literal["unknown","member","trusted","anchor"]` | | | `Profile` | `Literal["anchor","hearth","spark","bridge"]` | | | `Stability` | `Literal["experimental","beta","stable"]` | | | `Mode` | `Literal["online","degraded","offline"]` | emergency state | | `Category` | `Literal["offer","request","info","emergency"]` | marketplace post | | `EventType` | `Literal[...]` | 19 values; see [X02 §3.1](cross-cutting/X02-events.md) | ### 0.2 Constants — `hearthnet/constants.py` Hardcoded; never configurable. Complete list in [GLOSSARY.md §Defaults](GLOSSARY.md). `MANIFEST_TTL_SECONDS=30`, `MANIFEST_REPUBLISH_INTERVAL_SECONDS=20`, `DISCOVERY_UDP_INTERVAL_SECONDS` (5 active / 30 stable), `EMERGENCY_PROBE_INTERVAL_ONLINE=10`, `EMERGENCY_PROBE_INTERVAL_OFFLINE=2`, `EMERGENCY_PROBE_TIMEOUT_SECONDS=2`, `EMERGENCY_TRANSITION_DEBOUNCE_SECONDS=30`, `CONNECTION_IDLE_SECONDS=60`, `RECONNECT_BACKOFF_CAP_SECONDS=30`, `STREAM_WINDOW_FRAMES=16`, `STREAM_ACK_INTERVAL_FRAMES=8`, `STREAM_ACK_TIMEOUT_SECONDS=5`, `RPC_DEFAULT_TIMEOUT_SECONDS=30`, `LLM_GENERATION_DEFAULT_TIMEOUT_SECONDS=120`, `CHUNK_SIZE_BYTES=262144`, `BLOB_GC_DISK_THRESHOLD=0.80`, `RAG_CHUNK_TOKENS=1000`, `RAG_CHUNK_OVERLAP_TOKENS=200`, `RAG_DEFAULT_K=5`, `RAG_MAX_K=20`, `HEALTH_WINDOW_CALLS=100`, `HEALTH_QUARANTINE_THRESHOLD=0.5`, `HEALTH_QUARANTINE_SECONDS=60`, `RATE_LIMIT_SOFT_RPS_PER_CAP=10`, `RATE_LIMIT_HARD_RPS_PER_CAP=100`, `RATE_LIMIT_SOFT_RPS_TOTAL=100`, `RATE_LIMIT_HARD_RPS_TOTAL=1000`, `EVENT_LOG_RETENTION_DAYS=30`, `SNAPSHOT_LAG_LAMPORT=1000`, `TRACE_RING_BUFFER=10000`, `LOG_RETENTION_DAYS=14`. ### 0.3 Naming rules - Functions: `snake_case`, verb-first - Classes: `PascalCase`, noun - Constants: `SCREAMING_SNAKE` - Async I/O: `async def`; no `async_` prefix on names - Protocols: `PascalCase` ending in capability noun (`LlmBackend`, `Service`) - Private: leading underscore (`_compute_canonical_json`) ### 0.4 Universal error code → wire code mapping | Domain exception | Wire `ErrorCode` | HTTP | |------------------|------------------|------| | `IdentityError("invalid_signature")` | `invalid_signature` | 401 | | `IdentityError("expired")` | `expired` | 410 | | `BusError("schema_mismatch")` | `schema_mismatch` | 400 | | `BusError("not_found")` | `not_found` | 404 | | `BusError("capacity_exceeded")` | `capacity_exceeded` | 429 | | `BusError("quarantined" / "partition")` | `partition` | 503 | | `BusError("timeout")` | `timeout` | 408 | | `EventLogError("invalid_signature")` | `invalid_signature` | — (internal) | | `BlobError("not_found")` | `not_found` | 404 | | `BlobError("hash_mismatch")` | `bad_request` | 400 | | `BlobError("disk_full")` | `capacity_exceeded` | 429 | | `OnboardingError(*)` | — (local UI only) | — | | `ConfigError(*)` | — (startup) | — | --- ## 1. X04 — Configuration **Spec:** [`cross-cutting/X04-config.md`](cross-cutting/X04-config.md) · **Path:** `hearthnet/config.py` + `hearthnet/constants.py` ### `hearthnet/config.py` #### Dataclasses (all `@dataclass(frozen=True)`) `IdentityConfig` — §3: - `keys_dir: Path` - `auto_generate: bool = True` `CommunityConfig` — §3: - `community_id: Optional[str] = None` - `state_dir: Path = Path()` `TransportConfig` — §3: - `host: str = "0.0.0.0"` - `port: int = 7080` - `tls_cert: Optional[Path] = None` - `tls_key: Optional[Path] = None` `DiscoveryConfig` — §3: - `mdns_enabled: bool = True` - `udp_enabled: bool = True` - `udp_multicast_group: str = "239.255.42.42"` - `udp_port: int = 42424` - `relay_urls: list[str] = []` `BusConfig` — §3: - `prefer_local: bool = True` - `local_load_threshold: float = 0.80` `LlmBackendConfig` — §3: - `name: str` — one of `"llama_cpp" | "ollama" | "lmstudio" | "vllm" | "hf_api" | "anthropic_api" | "nemotron" | "openbmb"` - `url: Optional[str] = None` - `model: Optional[str] = None` - `api_key_env: Optional[str] = None` `LlmConfig` — §3: - `backends: list[LlmBackendConfig] = []` `EmbeddingConfig` — §3: - `model: str = "BAAI/bge-small-en-v1.5"` - `device: str = "auto"` `RagConfig` — §3: - `enabled: bool = True` - `corpora_dir: Path = Path()` `FileConfig` — §3: - `blobs_dir: Path = Path()` - `gc_threshold: float = 0.80` `MarketConfig` — §3: - `enabled: bool = True` - `default_ttl_seconds: int = 604800` - `max_ttl_seconds: int = 2592000` `ChatConfig` — §3: - `enabled: bool = True` - `store_and_forward: bool = True` `EmergencyConfig` — §3: - `probe_targets: list[str] = ["1.1.1.1","8.8.8.8","cloudflare.com","quad9.net"]` `UiConfig` — §3: - `host: str = "127.0.0.1"` - `port: int = 7860` - `launch_browser: bool = True` `ObservabilityConfig` — §3 (+ trackio addition): - `log_level: str = "info"` - `log_dir: Path = Path()` - `metrics_enabled: bool = True` - `otlp_endpoint: Optional[str] = None` - `trackio_project: Optional[str] = None` — local trackio project name; enables trackio exporter when set - `trackio_space: Optional[str] = None` — HF Space URL to mirror trackio runs to; optional `Config` — §3: - holds one of each of the above as named attributes #### Functions - `load(path: Path | None = None) -> Config` — §4. Read TOML, apply defaults, resolve paths, validate. Raises `ConfigError`. - `default_config() -> Config` — §4. All-defaults Config. - `save(config: Config, path: Path | None = None) -> None` — §4. Atomic TOML write. - `resolve_paths(config: Config) -> Config` — §4. Resolve empty `Path()` to XDG locations. Idempotent. - `validate(config: Config) -> None` — §4. Cross-field checks; raises `ConfigError`. #### Exception `ConfigError(Exception)` — §4: - `__init__(code: str, **details)` - `code: str` - `details: dict` ### `hearthnet/constants.py` Module-level constants from [GLOSSARY.md §Defaults](GLOSSARY.md). No classes; just `NAME = value` lines. --- ## 2. X03 — Observability **Spec:** [`cross-cutting/X03-observability.md`](cross-cutting/X03-observability.md) · **Path:** `hearthnet/observability/` ### `hearthnet/observability/logging.py` — §3 #### Functions - `configure(config: ObservabilityConfig) -> None` — install handlers + rotation; idempotent. - `get_logger(name: str) -> logging.Logger` — JSON-formatted logger. #### Class `JsonFormatter(logging.Formatter)` — §3.1: - `format(record: LogRecord) -> str` — emit `{"ts","level","logger","msg",**extras}`. `RateLimitedLogger` — §3.2 (internal wrapper): - `__init__(logger: Logger, per_key_seconds: float = 1.0)` - `info(msg: str, key: str, **extras) -> None` - `warning(msg: str, key: str, **extras) -> None` ### `hearthnet/observability/metrics.py` — §4 #### Functions - `configure(config: ObservabilityConfig) -> None` — set up registries, start `/metrics` endpoint. - `counter(name: str, doc: str, labels: list[str] = []) -> Counter` - `histogram(name: str, doc: str, labels: list[str] = [], buckets: list[float] | None = None) -> Histogram` - `gauge(name: str, doc: str, labels: list[str] = []) -> Gauge` - `disabled() -> bool` — true when metrics are off. #### Standard metric set — §4.2 Pre-registered at startup with these exact names: `hearthnet_requests_total{capability,result}`, `hearthnet_request_duration_ms{capability,quantile}`, `hearthnet_active_streams{capability}`, `hearthnet_nodes_online{community}`, `hearthnet_event_log_size{community}`, `hearthnet_event_log_lamport_head{community}`, `hearthnet_emergency_mode{state}`, `hearthnet_blob_storage_bytes`, `hearthnet_llm_tokens_generated_total{model,backend}`, `hearthnet_llm_concurrent{model}`, `hearthnet_capability_health_success_rate{capability,node}`, `hearthnet_rate_limited_total{capability,reason}`, `hearthnet_signature_failures_total{reason}`, `hearthnet_quarantines_total`. #### Trackio integration (new) — §4.4 `TrackioExporter` — optional. Activated when `config.observability.trackio_project` is set. - `__init__(project: str, space: str | None = None)` — opens a trackio run. - `record_call(capability: str, model: str | None, latency_ms: float, tokens_in: int | None, tokens_out: int | None, result: str) -> None` — logs one inference call as a step in the run. - `record_topology_snapshot(snapshot: TopologySnapshot) -> None` — periodic mesh health log. - `close() -> None` Use trackio when you want a Gradio-native dashboard for run/inference history (alternative or complement to Prometheus). Bridged into `TraceHook.on_call_end` when active. ### `hearthnet/observability/tracing.py` — §5 #### Dataclasses `Trace` — §5.1: - `trace_id: str` (ULID) - `capability: str` - `started_at: float` - `spans: list[Span]` `Span` — §5.1: - `name: str` - `started_at: float` - `ended_at: float | None` - `extras: dict` #### Functions - `new_trace(capability: str) -> Trace` — open a new trace, attach to current task. - `current_trace() -> Trace | None` - `attach(trace: Trace) -> None` - `detach() -> None` — close the current trace, push to ring buffer. - `span(name: str, **extras) -> AbstractAsyncContextManager[Span]` — open a sub-span. - `get_recent(n: int = 100) -> list[Trace]` — read from ring buffer (size `TRACE_RING_BUFFER`). ### `hearthnet/observability/doctor.py` — §6 #### Dataclass `CheckResult` — §6.1: - `name: str` - `ok: bool` - `detail: str` - `fix: str | None` #### Functions - `register(name: str, check: Callable[[Config, CapabilityBus], CheckResult]) -> None` - `run_all(config: Config, bus: CapabilityBus) -> list[CheckResult]` - `run_one(name: str, config: Config, bus: CapabilityBus) -> CheckResult` #### Standard checks (registered at startup) — §6.2 `keys_present`, `keys_loadable`, `community_present`, `event_log_writable`, `mdns_socket`, `udp_multicast`, `transport_port`, `at_least_one_capability`, `disk_space`, `clock_sanity`, `llm_backend_reachable`, `recent_error_rate`. --- ## 3. X02 — Events **Spec:** [`cross-cutting/X02-events.md`](cross-cutting/X02-events.md) · **Path:** `hearthnet/events/` ### `hearthnet/events/types.py` — §3.1 `EventType` — Literal of 19 strings, exactly: `community.created`, `community.member.invited`, `community.member.joined`, `community.member.revoked`, `community.member.promoted`, `community.member.demoted`, `community.policy.updated`, `node.manifest.updated`, `market.post.created`, `market.post.updated`, `market.post.expired`, `chat.message.sent`, `chat.message.delivered`, `chat.message.read`, `file.cid.advertised`, `file.cid.unpinned`, `rag.document.ingested`, `federation.peer.added`, `federation.peer.removed`. `Event` *(frozen dataclass)*: - `schema_version: int` - `event_id: str` - `lamport: int` - `wall_clock: str` - `community_id: str` - `author: str` - `event_type: EventType` - `data: dict` - `signature: str` ### `hearthnet/events/lamport.py` — §3.2 `LamportClock`: - `__init__(conn: sqlite3.Connection, community_id: str)` — load current value. - `current: int` *(property)* - `tick_for_send() -> int` — increment + persist; returns new value. - `observe(received_lamport: int) -> None` — `max(current, received) + 1`. ### `hearthnet/events/log.py` — §3.3 `EventLog`: - `__init__(db_path: Path, community_id: str)` — open/create SQLite (WAL); apply schema. - `append_local(event_type: EventType, data: dict, author_kp: KeyPair) -> Event` — mint, sign, persist, fan out. - `append_received(event: Event) -> bool` — verify, persist if new. Returns True if new. - `head() -> int` — highest Lamport. - `get(event_id: str) -> Event | None` - `replay(*, since_lamport: int = 0, event_types: list[EventType] | None = None, limit: int | None = None) -> Iterator[Event]` - `heads_by_type() -> dict[EventType, int]` - `subscribe(event_types: list[EventType] | None = None) -> AsyncIterator[Event]` `EventLogError(Exception)`: - `code in {"invalid_signature","out_of_order","unknown_author","revoked_author","schema_unknown","db_corrupt"}` ### `hearthnet/events/replay.py` — §3.4 `MaterialisedView` *(Protocol)*: - `reset() -> None` - `apply(event: Event) -> None` - `snapshot_state() -> dict` - `restore_state(state: dict) -> None` `ReplayEngine`: - `__init__(log: EventLog)` - `register(name: str, view: MaterialisedView, event_types: list[EventType]) -> None` - `rebuild(view_name: str, from_lamport: int = 0) -> None` - `rebuild_all() -> None` - `on_event(event: Event) -> None` — wired from `EventLog`. ### `hearthnet/events/snapshot.py` — §3.5 `Snapshot` *(frozen dataclass)*: - `schema_version: int` - `community_id: str` - `lamport: int` - `wall_clock: str` - `state: dict` - `covers_events_up_to: int` - `signature: str` `SnapshotStore`: - `__init__(dir_path: Path, community_id: str)` - `latest() -> Snapshot | None` - `write(snap: Snapshot) -> None` — atomic. - `list() -> list[int]` - `prune(keep_last_n: int = 7) -> None` Free functions: - `build_snapshot(log: EventLog, engine: ReplayEngine, signing_kp: KeyPair, at_lamport: int | None = None) -> Snapshot` - `restore_from_snapshot(snap: Snapshot, engine: ReplayEngine, log: EventLog) -> None` ### `hearthnet/events/sync.py` — §3.6 `HeadsReport` *(frozen dataclass)*: - `community_id: str` - `heads_by_type: dict[EventType, int]` - `head: int` `SyncResult` *(frozen dataclass)*: - `sent_count: int` - `received_count: int` - `duration_ms: int` `SyncClient`: - `__init__(log: EventLog, transport_client: HttpClient)` - `sync_with(peer_endpoint: Endpoint) -> SyncResult` *(async)* - `run_round(peer_registry: PeerRegistry) -> list[SyncResult]` *(async)* — sync against all known peers. `SyncServer`: - `__init__(log: EventLog)` - `serve_heads() -> HeadsReport` *(async)* - `serve_events(events: list[Event]) -> dict` *(async)* --- ## 4. X01 — Transport **Spec:** [`cross-cutting/X01-transport.md`](cross-cutting/X01-transport.md) · **Path:** `hearthnet/transport/` ### `hearthnet/transport/server.py` — §3 `HttpServer`: - `__init__(config: TransportConfig, kp: KeyPair, bus: CapabilityBus, event_sync: SyncServer, community_manifest_provider: Callable[[], CommunityManifest])` - `app() -> FastAPI` — for tests. - `run() -> None` *(async)* — block, serve. - `shutdown() -> None` *(async)* #### Mounted endpoints — §3.2 `POST /bus/v1/call`, `GET /manifest`, `GET /community/manifest`, `GET /sync/v1/heads`, `POST /sync/v1/events`, `GET /pubsub/v1/subscribe`, `GET /health`, `GET /ready`, `GET /metrics`, `GET /trace/recent`. `PubSubServer` — §8: - `publish(topic: str, payload: dict) -> None` *(async)* - `subscribe(topic: str, *, last_seq: int = 0, timeout_seconds: float = 30) -> dict` *(async)* — long-poll. ### `hearthnet/transport/client.py` — §5 `HttpClient`: - `__init__(kp: KeyPair, node_id: str, community_id: str, pinned_certs: PinnedCerts, timeout_default_seconds: float = RPC_DEFAULT_TIMEOUT_SECONDS)` - `call(peer: Endpoint, capability: str, version: str, body: dict, *, trace_id: str | None = None, timeout_seconds: float | None = None) -> dict` *(async)* — signed RPC. - `stream(peer: Endpoint, capability: str, version: str, body: dict, *, trace_id: str | None = None, cancel: asyncio.Event | None = None) -> AsyncIterator[Frame]` — signed stream. - `close() -> None` *(async)* `CallError(Exception)`: - `code: ErrorCode` - `message: str` - `retry_after_ms: int | None` - `alt_capabilities: list[str]` - `alt_nodes: list[str]` ### `hearthnet/transport/streams.py` — §6 `Frame` *(frozen dataclass)*: - `event: str` — `"token" | "chunk" | "progress" | "ack" | "done" | "error" | "manifest" | "ready" | "tool_call_delta"` - `data: dict` - `seq: int` `SseWriter`: - `__init__(response: StreamingResponse)` - `emit(event: str, data: dict) -> None` *(async)* - `emit_token(token: dict) -> None` *(async)* - `emit_progress(current: int, total: int, stage: str) -> None` *(async)* - `emit_error(code: ErrorCode, **kwargs) -> None` *(async)* - `emit_done(**meta) -> None` *(async)* - `emit_ack(upto: int) -> None` *(async)* - `cancelled: bool` *(property)* `SseReader`: - `__aiter__() -> AsyncIterator[Frame]` - `cancel() -> None` *(async)* ### `hearthnet/transport/backpressure.py` — §6.3 `FlowControl`: - `__init__(window: int = STREAM_WINDOW_FRAMES, ack_interval: int = STREAM_ACK_INTERVAL_FRAMES)` - `window_used: int` *(property)* - `send() -> None` *(async)* — await if window full. - `ack(upto: int) -> None` - `needs_ack: bool` *(property)* ### `hearthnet/transport/tls.py` — §4 `PinnedCerts`: - `__init__(db_path: Path)` - `record(node_id: str, fingerprint: bytes) -> None` - `expected(node_id: str) -> bytes | None` - `verify(node_id: str, presented: bytes) -> bool` ### `hearthnet/transport/__init__.py` — §7 `RateCheck` *(frozen dataclass)*: - `allowed: bool` - `soft_exceeded: bool` - `retry_after_ms: int` `RateLimiter`: - `__init__(config: TransportConfig)` - `check(peer_node_id: str, capability: str) -> RateCheck` --- ## 5. M01 — Identity & Manifests **Spec:** [`modules/M01-identity.md`](modules/M01-identity.md) · **Path:** `hearthnet/identity/` ### `hearthnet/identity/keys.py` — §3.1 #### Class `KeyPair` *(frozen dataclass)*: - `signing_key: nacl.signing.SigningKey` - `verify_key: nacl.signing.VerifyKey` - `node_id_full: str` - `node_id_short: str` - `sign(payload: dict) -> dict` — returns `payload` + `signature` field. - `sign_bytes(data: bytes) -> Signature` #### Functions - `generate() -> KeyPair` - `load(keys_dir: Path) -> KeyPair` — raises `IdentityError("keys_missing"|"keys_invalid"|"keys_permissions")`. - `load_or_generate(keys_dir: Path) -> KeyPair` - `save(kp: KeyPair, keys_dir: Path) -> None` — 0600 perms. - `short_node_id(verify_key_bytes: bytes) -> str` — `"ed25519:XXXX-XXXX-XXXX-XXXX"`. - `full_node_id(verify_key_bytes: bytes) -> str` — `"ed25519:"`. - `parse_node_id(node_id: str) -> bytes` — accepts only full form. - `verify_key_from_full(node_id_full: str) -> VerifyKey` - `canonical_json(obj: Any) -> bytes` — sorted, no whitespace, no trailing zeros, UTF-8. - `sign_payload(payload: dict, kp: KeyPair) -> dict` - `verify_payload(payload: dict, vk: VerifyKey) -> bool` - `verify_payload_with_node_id(payload: dict, expected_node_id_full: str) -> bool` - `generate_self_signed_cert(kp: KeyPair, host: str = "0.0.0.0") -> tuple[bytes, bytes]` — `(cert_pem, key_pem)`, 10-year validity. #### Exception `IdentityError(Exception)`: - `code in {"keys_missing","keys_invalid","keys_permissions","bad_node_id","sign_failed","verify_failed","bad_manifest","expired","invalid_signature"}` ### `hearthnet/identity/manifest.py` — §3.2 #### Dataclasses `Endpoint` *(frozen)*: `transport: str`, `host: str`, `port: int`. `HardwareSpec` *(frozen)*: `gpu: str | None`, `vram_gb: float`, `ram_gb: float`, `cpu_cores: int`, `disk_free_gb: float`. `CapabilitySpec` *(frozen)* — subset of `CapabilityDescriptor` for manifest embedding: - `name: str`, `version: str`, `stability: str`, `schema_hash: str`, `params: dict`, `max_concurrent: int`. `NodeManifest` *(frozen)*: - `version: int`, `contract_version: str`, `node_id: str`, `display_name: str`, `community_id: str`, `profile: str`, `endpoints: list[Endpoint]`, `hardware: HardwareSpec`, `capabilities: list[CapabilitySpec]`, `uptime_seconds: int`, `load: dict`, `issued_at: str`, `expires_at: str`, `signature: str`. - `as_dict() -> dict` - `is_expired(now: datetime | None = None) -> bool` `CommunityPolicy` *(frozen)*: - `min_signatures_to_invite: int` - `min_signatures_to_demote: int` - `min_signatures_to_revoke: int` - `capability_token_ttl_seconds: int` - `federation_enabled: bool` - `default_member_can_invite: bool` `CommunityMember` *(frozen)*: `node_id: str`, `level: TrustLevel`, `added_at: str`, `added_by: str`. `RevokedEntry` *(frozen)*: `node_id: str`, `revoked_at: str`. `CommunityManifest` *(frozen)*: - `version: int`, `community_id: str`, `name: str`, `root_key: str`, `created_at: str`, `lamport_at_creation: int`, `policy: CommunityPolicy`, `members: list[CommunityMember]`, `revoked: list[RevokedEntry]`, `head_lamport: int`, `signature: str`. - `is_member(node_id: str) -> bool` - `level_of(node_id: str) -> TrustLevel | None` - `is_revoked(node_id: str) -> bool` #### Functions - `build_node_manifest(kp: KeyPair, community_id: str, display_name: str, profile: str, endpoints: list[Endpoint], hardware: HardwareSpec, capabilities: list[CapabilitySpec], uptime_seconds: int, load: dict) -> NodeManifest` - `parse_node_manifest(blob: bytes | dict) -> NodeManifest` - `verify_node_manifest(manifest: NodeManifest, *, now: datetime | None = None) -> None` - `build_community_manifest(root_kp: KeyPair, name: str, policy: CommunityPolicy) -> CommunityManifest` - `regenerate_community_manifest_from_state(materialised_state: dict, signing_kp: KeyPair) -> CommunityManifest` - `parse_community_manifest(blob: bytes | dict) -> CommunityManifest` - `verify_community_manifest(cm: CommunityManifest) -> None` - `load_or_regenerate(state_dir: Path, signing_kp: KeyPair | None = None) -> CommunityManifest` — convenience used by `node.py`. ### `hearthnet/identity/tokens.py` — §3.3 *(Phase 2; stub in MVP)* `CapabilityToken` *(frozen)*: `issuer: str`, `subject: str`, `capability: str`, `issued_at: str`, `expires_at: str`, `nonce: str`, `signature: str`. Functions (stubs): - `issue_token(issuer_kp: KeyPair, subject_node_id: str, capability: str, ttl_seconds: int = 86400) -> CapabilityToken` - `verify_token(token: CapabilityToken, expected_issuer: str) -> None` --- ## 6. M02 — Discovery **Spec:** [`modules/M02-discovery.md`](modules/M02-discovery.md) · **Path:** `hearthnet/discovery/` ### `hearthnet/discovery/peers.py` — §3.1 `PeerRecord` *(dataclass)*: - `node_id: str` (short), `node_id_full: str`, `display_name: str`, `community_id: str`, `profile: Profile`, `endpoints: list[Endpoint]`, `manifest: NodeManifest | None`, `last_seen: float`, `rtt_ms: float | None`, `source: str` (`"mdns"|"udp"|"relay"`). `PeerEvent` *(frozen)*: `kind: str` (`"added"|"removed"|"updated"`), `peer: PeerRecord`. `PeerRegistry`: - `__init__(our_node_id_full: str, community_id: str)` - `upsert(record: PeerRecord) -> bool` — True if new. - `remove(node_id_full: str) -> bool` - `get(node_id_full: str) -> PeerRecord | None` - `all() -> list[PeerRecord]` - `for_community(community_id: str) -> list[PeerRecord]` - `prune_stale(max_age_seconds: int = 90) -> int` - `subscribe() -> AsyncIterator[PeerEvent]` - `set_pruning_aggressive(enabled: bool) -> None` — toggled by M09; uses 30s when on, 90s when off. ### `hearthnet/discovery/mdns.py` — §3.2 `MdnsAnnouncer`: - `__init__(kp: KeyPair, node_id_short: str, display_name: str, community_id_short: str, profile: Profile, port: int, capabilities_names: list[str], manifest_url: str)` - `start() -> None` *(async)* - `stop() -> None` *(async)* - `update(*, capabilities_names: list[str] | None = None) -> None` `MdnsBrowser`: - `__init__(registry: PeerRegistry, our_community_id: str)` - `start() -> None` *(async)* - `stop() -> None` *(async)* ### `hearthnet/discovery/udp.py` — §3.4 `UdpAnnouncer`: - `__init__(kp: KeyPair, registry: PeerRegistry, node_id_short: str, community_id_short: str, port: int, capabilities_names: list[str], multicast_group: str = "239.255.42.42", multicast_port: int = 42424)` - `run() -> None` *(async)* `UdpListener`: - `__init__(registry: PeerRegistry, our_community_id: str)` - `run() -> None` *(async)* ### `hearthnet/discovery/relay.py` *(Phase 2 stub)* `InternetRelayClient` — not implemented in MVP. Reserved. #### Exception `DiscoveryError(Exception)`: - `code in {"socket_in_use","mdns_unavailable","manifest_fetch_failed","manifest_invalid"}` --- ## 7. M03 — Capability Bus **Spec:** [`modules/M03-bus.md`](modules/M03-bus.md) · **Path:** `hearthnet/bus/` ### `hearthnet/bus/capability.py` — §3.1 `CapabilityDescriptor` *(frozen dataclass)*: - `name: CapabilityName`, `version: Version`, `stability: Stability`, `request_schema: dict`, `response_schema: dict | None`, `stream_schema: dict | None`, `params: dict`, `max_concurrent: int`, `trust_required: str` (`"member"|"trusted"|"anchor"|"self"`), `timeout_seconds: int`, `idempotent: bool`. - `version_str -> str` *(property)* - `schema_hash() -> str` — BLAKE3 of canonical-JSON of `{name, version, request_schema, response_schema, stream_schema}`. `CapabilityEntry` *(dataclass)*: - `node_id: str`, `descriptor: CapabilityDescriptor`, `is_local: bool`, `handler: Callable | None`, `endpoint: Endpoint | None`, `in_flight: int`, `last_seen: float`, `p50_latency_ms: float`, `p99_latency_ms: float`, `success_rate: float`, `quarantined_until: float`, `sticky_sessions: set[str]`. `RouteRequest` *(frozen dataclass)*: - `capability: CapabilityName`, `version_req: Version`, `body: dict`, `caller: str`, `trace_id: str`, `session_id: str | None`, `deadline_ms: int`, `stream: bool`. `ParamsPredicate` — type alias: `Callable[[dict, dict], bool]`. ### `hearthnet/bus/registry.py` — §3.2 `Diff` *(frozen dataclass)*: `added`, `removed`, `updated` — each `list[CapabilityEntry]`. `RegistryEvent` *(frozen dataclass)*: `kind: str` (`"added"|"removed"|"updated"`), `entry: CapabilityEntry`. `Registry`: - `__init__(our_node_id: str)` - `register_local(descriptor: CapabilityDescriptor, handler: Callable, params_compatible: ParamsPredicate | None = None) -> None` - `deregister_local(name: CapabilityName, version: Version) -> None` - `update_from_peer_manifest(peer: PeerRecord, manifest: NodeManifest) -> Diff` - `remove_peer(node_id: str) -> int` - `find(name: CapabilityName, version_req: Version, params_filter: Callable[[dict], bool] | None = None) -> list[CapabilityEntry]` - `entry(node_id: str, name: CapabilityName, version: Version) -> CapabilityEntry | None` - `all_local() -> list[CapabilityEntry]` - `all() -> list[CapabilityEntry]` - `subscribe() -> AsyncIterator[RegistryEvent]` ### `hearthnet/bus/health.py` — §3.3 `HealthTracker`: - `__init__(window: int = HEALTH_WINDOW_CALLS)` - `record(entry: CapabilityEntry, *, success: bool, latency_ms: float) -> None` - `is_quarantined(entry: CapabilityEntry) -> bool` - `reset(entry: CapabilityEntry) -> None` ### `hearthnet/bus/schema.py` — §3.4 `SchemaValidator`: - `__init__()` - `validate_request(descriptor: CapabilityDescriptor, body: dict) -> None` - `validate_response(descriptor: CapabilityDescriptor, body: dict) -> None` - `validate_stream_frame(descriptor: CapabilityDescriptor, frame: dict) -> None` Free function: - `compute_schema_hash(descriptor_partial: dict) -> str` — `"blake3:"`. See [CONTRACT §11](CAPABILITY_CONTRACT.md). ### `hearthnet/bus/router.py` — §3.5 `Router`: - `__init__(registry: Registry, config: BusConfig, our_node_id: str)` - `route(req: RouteRequest) -> CapabilityEntry | None` — scoring algorithm; see §5.4. - `route_sticky(req: RouteRequest) -> CapabilityEntry | None` - `release_session(session_id: str) -> None` ### `hearthnet/bus/trace.py` — §3.6 `CallTraceEvent` *(frozen dataclass)*: - `ts: str`, `trace_id: str`, `capability: CapabilityName`, `version: str`, `from_node: str`, `to_node: str`, `is_local: bool`, `result: str`, `ms: float`, `tokens_in: int | None`, `tokens_out: int | None`, `bytes_in: int`, `bytes_out: int`. `TraceHook`: - `__init__()` - `on_call_start(req: RouteRequest, entry: CapabilityEntry) -> None` - `on_call_end(req: RouteRequest, entry: CapabilityEntry, *, result: str, latency_ms: float, bytes_in: int, bytes_out: int, tokens_in: int | None = None, tokens_out: int | None = None) -> None` ### `hearthnet/bus/__init__.py` — §3.7 `TopologySnapshot` *(frozen dataclass)*: - `our_node_id: str`, `peers: list[PeerRecord]`, `capabilities_local: list[CapabilityEntry]`, `capabilities_remote: list[CapabilityEntry]`, `in_flight_total: int`. `CapabilityBus`: - `__init__(node_id_full: str, community_id: str, config: BusConfig, transport_client: HttpClient, community_manifest_provider: Callable[[], CommunityManifest])` - attributes: `registry`, `health`, `schema`, `router`, `trace` - `register_service(service: Service) -> None` - `register_capability(descriptor: CapabilityDescriptor, handler: Callable, params_compatible: ParamsPredicate | None = None) -> None` - `handle_call(req: RouteRequest) -> dict | AsyncIterator[dict]` *(async)* - `call(capability: CapabilityName, version_req: Version, body: dict, *, session_id: str | None = None, timeout_seconds: float | None = None) -> dict` *(async)* - `stream(capability: CapabilityName, version_req: Version, body: dict, *, session_id: str | None = None) -> AsyncIterator[Frame]` - `on_peer_added(peer: PeerRecord) -> None` - `on_peer_updated(peer: PeerRecord) -> None` - `on_peer_removed(node_id: str) -> None` - `topology_snapshot() -> TopologySnapshot` - `recent_traces(n: int = 50) -> list[CallTraceEvent]` - `stats() -> dict` `BusError(Exception)`: - `code in {"schema_invalid","namespace_violation","schema_mismatch","not_found","capacity_exceeded","quarantined","partition","timeout","internal_error"}` ### `hearthnet/services/base.py` — M03 §4 `Service` *(Protocol)*: - `name: str` - `version: str` - `capabilities() -> list[tuple[CapabilityDescriptor, Callable, ParamsPredicate]]` - `start() -> None` *(async)* - `stop() -> None` *(async)* - `health() -> dict` --- ## 8. M11 — Embedding Service **Spec:** [`modules/M11-embedding.md`](modules/M11-embedding.md) · **Path:** `hearthnet/services/embedding/` ### `hearthnet/services/embedding/backends.py` — §3.1 `EmbeddingBackend` *(Protocol)*: - attrs: `name: str`, `model: str`, `dim: int`, `max_input: int` - `embed(texts: list[str], *, normalize: bool = True) -> list[list[float]]` *(async)* - `warm() -> None` *(async)* - `close() -> None` *(async)* - `health() -> dict` `SentenceTransformerBackend`: - `__init__(model: str, device: str = "auto")` — `device` ∈ `{"auto","cpu","cuda"}`. - all `EmbeddingBackend` methods. ### `hearthnet/services/embedding/service.py` — §3.2 `EmbeddingService` *(implements `Service`)*: - `name = "embedding"`, `version = "1.0"` - `__init__(config: EmbeddingConfig)` - `capabilities() -> [...]` — registers `embed.text@1.0`. - `start()`, `stop()`, `health()` *(async)* - `handle_embed_text(req: RouteRequest) -> dict` *(async)* — implements [CONTRACT §4.3](CAPABILITY_CONTRACT.md). #### Capability params predicate ```python def params_compatible(offered: dict, requested: dict) -> bool: return requested.get("model") == offered.get("model") ``` --- ## 9. M04 — LLM Service **Spec:** [`modules/M04-llm.md`](modules/M04-llm.md) · **Path:** `hearthnet/services/llm/` ### `hearthnet/services/llm/backends/base.py` — §3.1 `Token` *(frozen dataclass)*: `text: str`, `logprob: float | None`, `stop: bool`. `ChatResult` *(frozen dataclass)*: `text: str`, `tokens_in: int`, `tokens_out: int`, `stop_reason: str`, `ms: int`. `BackendModel` *(frozen dataclass)*: `name: str`, `quant: str`, `ctx_max: int`, `modalities: list[str]`, `requires_internet: bool`. `LlmBackend` *(Protocol)*: - attrs: `name: str`, `models: list[BackendModel]` - `warm(model: str) -> None` *(async)* - `close() -> None` *(async)* - `chat(*, model: str, messages: list[dict], max_tokens: int = 1024, temperature: float = 0.7, top_p: float = 0.95, stop: list[str] | None = None, seed: int | None = None, stream: bool = True) -> AsyncIterator[Token]` - `complete(*, model: str, prompt: str, max_tokens: int = 256, temperature: float = 0.7, top_p: float = 0.95, stop: list[str] | None = None, seed: int | None = None, stream: bool = True) -> AsyncIterator[Token]` - `count_tokens(model: str, text: str) -> int` - `max_concurrent(model: str) -> int` - `health() -> dict` ### Concrete backends — §3.2 Each implements `LlmBackend`. Same method set; only constructor varies. | File | Class | Constructor signature | |------|-------|-----------------------| | `backends/llama_cpp.py` | `LlamaCppBackend` | `__init__(model_path: Path, model_meta: BackendModel, gpu_layers: int = -1)` | | `backends/ollama.py` | `OllamaBackend` | `__init__(base_url: str = "http://localhost:11434", models: list[str] | None = None)` | | `backends/lmstudio.py` | `LmStudioBackend` | `__init__(base_url: str, default_model: str)` — OpenAI-compatible HTTP | | `backends/hf_api.py` | `HfApiBackend` | `__init__(model: str, token_env: str = "HF_TOKEN")` — `requires_internet=True` | | `backends/anthropic_api.py` | `AnthropicApiBackend` | `__init__(model: str = "claude-sonnet-4-6", token_env: str = "ANTHROPIC_API_KEY")` — `requires_internet=True` | | `backends/nemotron.py` *(new)* | `NemotronBackend` | `__init__(base_url: str = "https://integrate.api.nvidia.com/v1", model: str = "nvidia/llama-3.1-nemotron-70b-instruct", token_env: str = "NVIDIA_API_KEY", local: bool = False)` — OpenAI-compatible; `requires_internet=True` unless `local=True` (locally-hosted NIM endpoint) | | `backends/openbmb.py` *(new)* | `OpenBmbBackend` | `__init__(base_url: str = "http://localhost:8000", model: str = "openbmb/MiniCPM4-8B", token_env: str | None = None)` — OpenAI-compatible HTTP (vLLM / llama.cpp serve / SGLang); `requires_internet=False`. Designed around Christof's MiniCPM workbench | All backends declare their `models: list[BackendModel]` so the service can enumerate `(backend, model)` pairs at registration time. ### `hearthnet/services/llm/tokenizers.py` — §3.3 - `count_tokens_approx(model_family: str, text: str) -> int` - `model_family(model_name: str) -> str` — e.g. `"qwen2.5-7b-instruct"` → `"qwen"`, `"nemotron-70b"` → `"nemotron"`, `"MiniCPM4-8B"` → `"minicpm"`. ### `hearthnet/services/llm/service.py` — §3.4 `LlmService` *(implements `Service`)*: - `name = "llm"`, `version = "1.0"` - `__init__(config: LlmConfig)` - `_build_backends(config: LlmConfig) -> list[LlmBackend]` - `capabilities() -> [...]` — emits one descriptor per `(backend, model)` × `{llm.chat, llm.complete}`. - `start(), stop(), health()` *(async)* - `handle_chat(req: RouteRequest) -> AsyncIterator[dict]` *(async)* — implements [CONTRACT §4.1](CAPABILITY_CONTRACT.md). - `handle_complete(req: RouteRequest) -> AsyncIterator[dict]` *(async)* — implements [CONTRACT §4.2](CAPABILITY_CONTRACT.md). #### Capability params predicate — §3.6 ```python def params_compatible(offered: dict, requested: dict) -> bool: if requested.get("model") != offered.get("model"): return False if "ctx" in requested and requested["ctx"] > offered["ctx"]: return False return True ``` --- ## 10. M05 — RAG Service **Spec:** [`modules/M05-rag.md`](modules/M05-rag.md) · **Path:** `hearthnet/services/rag/` ### `hearthnet/services/rag/chunker.py` — §3.1 `Chunk` *(frozen dataclass)*: `text: str`, `metadata: dict`. Functions: - `chunk_text(text: str, *, tokens_per_chunk: int = RAG_CHUNK_TOKENS, overlap_tokens: int = RAG_CHUNK_OVERLAP_TOKENS, metadata: dict | None = None) -> list[Chunk]` - `chunk_pdf(pdf_bytes: bytes, *, doc_metadata: dict) -> list[Chunk]` ### `hearthnet/services/rag/store.py` — §3.2 `ScoredChunk` *(frozen dataclass)*: `chunk: Chunk`, `score: float`. `CorpusStore`: - `__init__(corpora_dir: Path, corpus: str, embedding_dim: int)` - `add_chunks(chunks: list[Chunk], embeddings: list[list[float]]) -> None` - `has_document(doc_cid: str) -> bool` - `query(embedding: list[float], *, k: int, filter: dict | None = None) -> list[ScoredChunk]` - `count() -> int` - `size_bytes() -> int` - `language_majority() -> str | None` Free functions: - `list_corpora(corpora_dir: Path) -> list[str]` - `corpus_info(corpora_dir: Path, corpus: str) -> dict` ### `hearthnet/services/rag/ingest.py` — §3.3 `IngestResult` *(frozen dataclass)*: `doc_cid: str`, `chunks_indexed: int`, `tokens_indexed: int`, `ingest_event_id: str`, `ms: int`. `IngestPipeline`: - `__init__(bus: CapabilityBus, blob_store: BlobStore, corpora_dir: Path, event_log: EventLog)` - `ingest_document(doc_cid: str, corpus: str, title: str, language: str, metadata: dict, author_kp: KeyPair) -> IngestResult` *(async)* ### `hearthnet/services/rag/service.py` — §3.4 `RagService` *(implements `Service`)*: - `name = "rag"`, `version = "1.0"` - `__init__(config: RagConfig, bus: CapabilityBus, blob_store: BlobStore, event_log: EventLog, community_manifest_provider: Callable[[], CommunityManifest])` - `capabilities() -> [...]` — `rag.query@1.0` per corpus, `rag.ingest@1.0` once, `rag.list_corpora@1.0` once. - `start(), stop(), health()` *(async)* - `handle_query(req: RouteRequest) -> dict` *(async)* — [CONTRACT §4.4](CAPABILITY_CONTRACT.md). - `handle_ingest(req: RouteRequest) -> dict` *(async)* — [CONTRACT §4.5](CAPABILITY_CONTRACT.md). - `handle_list_corpora(req: RouteRequest) -> dict` *(async)* — [CONTRACT §4.6](CAPABILITY_CONTRACT.md). #### Capability params predicate — §3.5 ```python def query_params_compatible(offered: dict, requested: dict) -> bool: return requested.get("corpus") == offered.get("corpus") ``` --- ## 11. M07 — File & Blobs **Spec:** [`modules/M07-file-blobs.md`](modules/M07-file-blobs.md) · **Paths:** `hearthnet/blobs/` + `hearthnet/services/file/` ### `hearthnet/blobs/chunker.py` — §3.1 `ChunkRef` *(frozen)*: `index: int`, `cid: str`, `size_bytes: int`. `BlobManifest` *(frozen)*: `cid: str`, `size_bytes: int`, `chunk_size_bytes: int`, `chunks: list[ChunkRef]`, `mime_type: str | None`, `filename: str | None`. Functions: - `hash_bytes(data: bytes) -> str` — `"blake3:"`. - `chunk_blob(data: bytes, *, chunk_size: int = CHUNK_SIZE_BYTES) -> tuple[BlobManifest, list[bytes]]` - `manifest_cid(manifest: BlobManifest) -> str` - `reassemble(chunks: list[bytes]) -> bytes` - `verify_chunk(data: bytes, expected_cid: str) -> None` — raises `BlobError("hash_mismatch")`. ### `hearthnet/blobs/store.py` — §3.2 `BlobStore`: - `__init__(dir_path: Path, gc_threshold: float = BLOB_GC_DISK_THRESHOLD)` - `has(cid: str) -> bool` - `read_chunk(cid: str) -> bytes` - `write_chunk(cid: str, data: bytes) -> None` - `delete_chunk(cid: str) -> bool` - `has_blob(manifest_cid: str) -> bool` - `read_manifest(manifest_cid: str) -> BlobManifest` - `write_blob(manifest: BlobManifest, chunks: list[bytes]) -> None` - `read_blob_bytes(manifest_cid: str) -> bytes` - `read_blob_stream(manifest_cid: str) -> AsyncIterator[tuple[ChunkRef, bytes]]` *(async)* - `list_cids(prefix: str | None = None) -> list[str]` - `total_bytes() -> int` - `pin(cid: str) -> None` - `unpin(cid: str) -> None` - `is_pinned(cid: str) -> bool` - `gc(target_fraction: float = 0.7) -> int` — bytes freed. `BlobError(Exception)`: - `code in {"not_found","hash_mismatch","io_error","disk_full","manifest_invalid"}` ### `hearthnet/blobs/transfer.py` — §3.3 `TransferManager`: - `__init__(store: BlobStore, bus: CapabilityBus, concurrency: int = 4)` - `fetch_blob(manifest_cid: str, *, sources: list[str] | None = None) -> BlobManifest` *(async)* - `advertise(cids: list[str]) -> None` *(async)* ### `hearthnet/services/file/service.py` — §4.1 `FileService` *(implements `Service`)*: - `name = "file"`, `version = "1.0"` - `__init__(config: FileConfig, store: BlobStore, event_log: EventLog)` - `capabilities() -> [...]` — `file.read`, `file.list`, `file.advertise`, `file.put` (all `@1.0`). - `start(), stop(), health()` *(async)* - `handle_read(req: RouteRequest) -> AsyncIterator[dict] | dict` *(async)* — [CONTRACT §4.7](CAPABILITY_CONTRACT.md). - `handle_list(req: RouteRequest) -> dict` *(async)* — [CONTRACT §4.8](CAPABILITY_CONTRACT.md). - `handle_advertise(req: RouteRequest) -> dict` *(async)* — [CONTRACT §4.9](CAPABILITY_CONTRACT.md). - `handle_put(req: RouteRequest) -> AsyncIterator[dict]` *(async)* — [CONTRACT §4.10](CAPABILITY_CONTRACT.md). All four `file.*` use default `lambda offered, requested: True` as params predicate. --- ## 12. M06 — Marketplace Service **Spec:** [`modules/M06-marketplace.md`](modules/M06-marketplace.md) · **Path:** `hearthnet/services/marketplace/` ### `hearthnet/services/marketplace/post.py` — §3.1 `Location` *(frozen dataclass)*: `lat: float`, `lng: float`, `label: str`. `Post` *(frozen dataclass)*: - `event_id: str`, `lamport: int`, `author: str`, `category: Category`, `title: str`, `body: str`, `location: Location | None`, `tags: list[str]`, `created_at: str`, `expires_at: str`, `expired_via_event_id: str | None`, `expiry_reason: str | None`. - `is_expired(now: datetime | None = None) -> bool` ### `hearthnet/services/marketplace/views.py` — §3.2 `MarketplaceView` *(implements `MaterialisedView` from X02)*: - `__init__()` - `reset() -> None` - `apply(event: Event) -> None` - `snapshot_state() -> dict` - `restore_state(state: dict) -> None` - `list(*, category: Category | None = None, tags: list[str] | None = None, since_lamport: int = 0, limit: int = 50) -> list[Post]` - `get(event_id: str) -> Post | None` - `max_lamport() -> int` - `all_active() -> list[Post]` ### `hearthnet/services/marketplace/service.py` — §3.3 `MarketplaceService` *(implements `Service`)*: - `name = "marketplace"`, `version = "1.0"` - `__init__(config: MarketConfig, bus: CapabilityBus, event_log: EventLog, replay_engine: ReplayEngine, author_kp: KeyPair, community_manifest_provider: Callable[[], CommunityManifest])` - `capabilities() -> [...]` — `market.list`, `market.post`, `market.expire`, `market.search` (all `@1.0`). - `start(), stop(), health()` *(async)* — start replays events and installs auto-expiry sweeper. - `handle_list(req) -> dict` *(async)* — [CONTRACT §4.11](CAPABILITY_CONTRACT.md). - `handle_post(req) -> dict` *(async)* — [CONTRACT §4.12](CAPABILITY_CONTRACT.md). - `handle_expire(req) -> dict` *(async)* — [CONTRACT §4.13](CAPABILITY_CONTRACT.md). - `handle_search(req) -> dict` *(async)* — [CONTRACT §4.14](CAPABILITY_CONTRACT.md). - `_auto_expire_sweep() -> None` *(async)* — internal background task. All four use default `lambda offered, requested: True` predicate. --- ## 13. M10 — Chat Service **Spec:** [`modules/M10-chat.md`](modules/M10-chat.md) · **Path:** `hearthnet/services/chat/` ### `hearthnet/services/chat/views.py` — §3.1 `ChatMessage` *(frozen dataclass)*: - `event_id: str`, `lamport: int`, `sender: str`, `recipient: str`, `body: str`, `attachments: list[dict]`, `created_at: str`, `delivered_at: str | None`, `read_at: str | None`. `ChatView` *(implements `MaterialisedView`)*: - `__init__(our_node_id_full: str)` - `reset(), apply(event), snapshot_state(), restore_state(state)` - `history_with(peer: str | None = None, *, since_lamport: int = 0, limit: int = 200) -> list[ChatMessage]` - `peers() -> list[str]` - `unread_count(peer: str) -> int` ### `hearthnet/services/chat/delivery.py` — §3.2 `DeliveryManager`: - `__init__(bus: CapabilityBus, event_log: EventLog, author_kp: KeyPair, peer_registry: PeerRegistry, config: ChatConfig)` - `deliver(message_event: Event) -> str` *(async)* — returns `"direct"|"forwarded"|"queued"`. - `on_local_message_arrived(message_event: Event) -> None` *(async)* - `on_pubsub_message(payload: dict) -> None` *(async)* ### `hearthnet/services/chat/service.py` — §3.3 `ChatService` *(implements `Service`)*: - `name = "chat"`, `version = "1.0"` - `__init__(config: ChatConfig, bus: CapabilityBus, event_log: EventLog, replay_engine: ReplayEngine, peer_registry: PeerRegistry, author_kp: KeyPair, our_node_id_full: str)` - `capabilities() -> [...]` — `chat.send@1.0` (member trust), `chat.history@1.0` (self trust). - `start(), stop(), health()` *(async)* - `handle_send(req) -> dict` *(async)* — [CONTRACT §4.15](CAPABILITY_CONTRACT.md). - `handle_history(req) -> dict` *(async)* — [CONTRACT §4.16](CAPABILITY_CONTRACT.md). Enforces `caller == our_node_id_full`. --- ## 14. M09 — Emergency Mode Detector **Spec:** [`modules/M09-emergency.md`](modules/M09-emergency.md) · **Path:** `hearthnet/emergency/` ### `hearthnet/emergency/state.py` — §3.1 `EmergencyState` *(frozen dataclass)*: - `mode: Mode`, `since: WallClock`, `last_probe: WallClock`, `probe_results: dict[str, bool]`. `StateBus`: - `__init__()` - `current() -> EmergencyState` - `subscribe() -> AsyncIterator[EmergencyState]` - `_emit(state: EmergencyState) -> None` *(internal)* ### `hearthnet/emergency/detector.py` — §3.2 `Detector`: - `__init__(config: EmergencyConfig, bus: CapabilityBus, state_bus: StateBus)` - `run() -> None` *(async)* - `shutdown() -> None` *(async)* - `_probe_dns(host: str) -> bool` *(async, internal)* - `_probe_http(url: str) -> bool` *(async, internal)* State-transition effects (§5.2): - entering offline → deregister local capabilities whose descriptor `params.requires_internet == True` - entering online → re-register those backends - offline ↔ online → flip `peer_registry.set_pruning_aggressive(...)` (M02) --- ## 15. M08 — UI **Spec:** [`modules/M08-ui.md`](modules/M08-ui.md) · **Path:** `hearthnet/ui/` ### `hearthnet/ui/app.py` — §3.1 `UiApp`: - `__init__(bus: CapabilityBus, state_bus: StateBus, config: UiConfig, node_id_short: str, community_name: str)` - `build() -> gr.Blocks` - `launch_async() -> None` *(async)* - `shutdown() -> None` *(async)* Free function: - `build_ui(bus: CapabilityBus, state_bus: StateBus, config: UiConfig, **meta) -> UiApp` ### `hearthnet/ui/topology.py` — §3.2 `TopologyComponent`: - `__init__(bus: CapabilityBus)` - `render() -> gr.HTML` - `push_trace(event: CallTraceEvent) -> None` - `push_topology(snapshot: TopologySnapshot) -> None` ### `hearthnet/ui/theme.py` — §7 - `hearthnet_theme: gr.Theme` *(module-level constant)* - `emergency_theme: gr.Theme` *(module-level constant)* - CSS variables documented in spec §7 ### `hearthnet/ui/tabs/` Each file exports a builder function returning a `gr.Tab` or `gr.Blocks` fragment. | File | Function | Spec | |------|----------|------| | `tabs/ask.py` | `build_ask_tab(bus: CapabilityBus) -> gr.Tab` | §5.1 | | `tabs/chat.py` | `build_chat_tab(bus: CapabilityBus, our_node_id_full: str) -> gr.Tab` | §5.3 | | `tabs/marketplace.py` | `build_marketplace_tab(bus: CapabilityBus) -> gr.Tab` | §5.4 | | `tabs/files.py` | `build_files_tab(bus: CapabilityBus) -> gr.Tab` | §5.5 | | `tabs/emergency.py` | `build_emergency_tab(bus: CapabilityBus, state_bus: StateBus) -> gr.Tab` | §5.6 | | `tabs/settings.py` | `build_settings_tab(bus: CapabilityBus, config: Config) -> gr.Tab` | §5.2 | ### `hearthnet/ui/mobile/` — §6 Static assets served at `/mobile/*` by [X01](cross-cutting/X01-transport.md): - `index.html` — single-page app - `app.js` — same bus API; uses signed requests via WebCrypto - `style.css` --- ## 16. M13 — Onboarding **Spec:** [`modules/M13-onboarding.md`](modules/M13-onboarding.md) · **Path:** `hearthnet/ui/onboarding.py` ### `hearthnet/ui/onboarding.py` — §3.1 `InviteBlob` *(frozen dataclass)*: - `schema_version: int`, `community_id: str`, `community_name: str`, `inviter_node_id: str`, `invitee_node_id: str`, `initial_level: str`, `bootstrap_endpoints: list[Endpoint]`, `expires_at: str`, `signature: str`. #### Functions - `encode_invite(blob: InviteBlob) -> str` — `"hearthnet://v1/"`. - `decode_invite(text: str) -> InviteBlob` - `invite_to_qr_png(blob: InviteBlob, *, box_size: int = 8) -> bytes` - `create_community(name: str, policy: CommunityPolicy, kp: KeyPair, state_dir: Path, event_log: EventLog) -> CommunityManifest` - `make_invite(invitee_node_id_full: str, inviter_kp: KeyPair, community_manifest: CommunityManifest, bootstrap_endpoints: list[Endpoint], initial_level: str = "member", ttl_seconds: int = 86400) -> InviteBlob` - `redeem_invite(blob: InviteBlob, our_kp: KeyPair, transport_client: HttpClient, event_log: EventLog) -> CommunityManifest` *(async)* - `build_onboarding(config: Config, kp_provider: Callable[[], KeyPair]) -> gr.Blocks` Exception: `OnboardingError(Exception)`: - `code in {"invite_invalid","invite_expired","invitee_mismatch","bootstrap_unreachable","community_manifest_invalid","sync_failed","already_member"}` --- ## 17. M12 — CLI & Orchestrator **Spec:** [`modules/M12-cli.md`](modules/M12-cli.md) · **Paths:** `hearthnet/cli.py` + `hearthnet/node.py` ### `hearthnet/cli.py` — §3 Click group + subcommands. Each is a top-level function decorated with `@main.command()`. | Command | Function | Spec | |---------|----------|------| | (root) | `main(ctx, config)` | §4 | | `init` | `init(name: str, profile: str, non_interactive: bool)` | §3.1 | | `run` | `run(config: str, no_ui: bool, debug: bool)` | §3.2 | | `status` | `status(json_output: bool)` | §3.3 | | `caps` | `caps(remote_only: bool, local_only: bool, name: str)` | §3.4 | | `call` | `call(name_at_version: str, body: str, stream: bool)` | §3.5 | | `log` | `log(follow: bool, level: str, component: str)` | §3.6 | | `trace` | `trace_recent(n: int, capability: str)` | §3.7 | | `doctor` | `doctor(check: str)` | §3.8 | | `export` | `export(out: str)` | §3.9 | | `erase` | `erase(keep_keys: bool, yes: bool)` | §3.10 | | `rag list` | `rag_list()` | §3.11 | | `rag ingest` | `rag_ingest(path: str, corpus: str)` | §3.11 | | `rag reindex` | `rag_reindex(corpus: str, embedding_model: str)` | §3.11 | | `invite create` | `invite_create(node_id: str, level: str, ttl: int)` | §3.12 | | `invite redeem` | `invite_redeem(text_or_path: str)` | §3.12 | | `version` | `version_cmd()` | §3.13 | Exit codes — §6: `0` success, `1` generic error, `2` user abort / bad usage, `3` no running node, `4` auth, `5` capacity. ### `hearthnet/node.py` — §5 Single function — the canonical wiring: ```python async def start(config: Config) -> None: """The 15-step composition. Do not deviate.""" ``` Sequence (each numbered in spec §5): 1. observability configure 2. identity load_or_generate 3. community check / onboarding redirect 4. event log + snapshot store + replay engine + community manifest 5. blob store 6. pinned-certs + transport client + bus 7. peer registry + mdns/udp announcer + listener 8. instantiate services (Embedding, Llm, Rag, File, Marketplace, Chat) and register with bus 9. state bus + Detector 10. http server 11. UI app 12. wire peer events → bus 13. ManifestPublisher 14. SyncClient periodic loop 15. asyncio.gather(...) — block until shutdown Auxiliary class declared inline in this module: `ManifestPublisher`: - `__init__(kp: KeyPair, community_manifest_provider: Callable, bus: CapabilityBus, peer_registry: PeerRegistry, interval_seconds: int = MANIFEST_REPUBLISH_INTERVAL_SECONDS)` - `run() -> None` *(async)* - Publishes the freshly-built node manifest to mDNS + UDP every `interval_seconds`. Triggered also on `bus.registry` change events (capability added/removed). `PeriodicTask` *(helper)*: - `__init__(fn: Callable[[], Awaitable], interval_seconds: int)` - `run() -> None` *(async)* ### `hearthnet/__main__.py` Single line: `from hearthnet.cli import main; main()` --- ## 18. Cross-module symbol index (alphabetical) For "where is `X` declared?" | Symbol | Module | File | |--------|--------|------| | `AnthropicApiBackend` | M04 | `services/llm/backends/anthropic_api.py` | | `BackendModel` | M04 | `services/llm/backends/base.py` | | `BlobError` | M07 | `blobs/store.py` | | `BlobManifest` | M07 | `blobs/chunker.py` | | `BlobStore` | M07 | `blobs/store.py` | | `BusConfig` | X04 | `config.py` | | `BusError` | M03 | `bus/__init__.py` | | `CallError` | X01 | `transport/client.py` | | `CallTraceEvent` | M03 | `bus/trace.py` | | `CapabilityBus` | M03 | `bus/__init__.py` | | `CapabilityDescriptor` | M03 | `bus/capability.py` | | `CapabilityEntry` | M03 | `bus/capability.py` | | `CapabilitySpec` | M01 | `identity/manifest.py` | | `CapabilityToken` | M01 | `identity/tokens.py` | | `Category` | M06 | `services/marketplace/post.py` (Literal alias) | | `ChatConfig` | X04 | `config.py` | | `ChatMessage` | M10 | `services/chat/views.py` | | `ChatService` | M10 | `services/chat/service.py` | | `ChatView` | M10 | `services/chat/views.py` | | `CheckResult` | X03 | `observability/doctor.py` | | `Chunk` | M05 | `services/rag/chunker.py` | | `ChunkRef` | M07 | `blobs/chunker.py` | | `CommunityConfig` | X04 | `config.py` | | `CommunityManifest` | M01 | `identity/manifest.py` | | `CommunityMember` | M01 | `identity/manifest.py` | | `CommunityPolicy` | M01 | `identity/manifest.py` | | `Config` | X04 | `config.py` | | `ConfigError` | X04 | `config.py` | | `CorpusStore` | M05 | `services/rag/store.py` | | `DeliveryManager` | M10 | `services/chat/delivery.py` | | `Detector` | M09 | `emergency/detector.py` | | `Diff` | M03 | `bus/registry.py` | | `DiscoveryConfig` | X04 | `config.py` | | `DiscoveryError` | M02 | `discovery/__init__.py` | | `EmbeddingBackend` | M11 | `services/embedding/backends.py` | | `EmbeddingConfig` | X04 | `config.py` | | `EmbeddingService` | M11 | `services/embedding/service.py` | | `EmergencyConfig` | X04 | `config.py` | | `EmergencyState` | M09 | `emergency/state.py` | | `Endpoint` | M01 | `identity/manifest.py` | | `Event` | X02 | `events/types.py` | | `EventLog` | X02 | `events/log.py` | | `EventLogError` | X02 | `events/log.py` | | `EventType` | X02 | `events/types.py` | | `FileConfig` | X04 | `config.py` | | `FileService` | M07 | `services/file/service.py` | | `FlowControl` | X01 | `transport/backpressure.py` | | `Frame` | X01 | `transport/streams.py` | | `HardwareSpec` | M01 | `identity/manifest.py` | | `HeadsReport` | X02 | `events/sync.py` | | `HealthTracker` | M03 | `bus/health.py` | | `HfApiBackend` | M04 | `services/llm/backends/hf_api.py` | | `HttpClient` | X01 | `transport/client.py` | | `HttpServer` | X01 | `transport/server.py` | | `IdentityConfig` | X04 | `config.py` | | `IdentityError` | M01 | `identity/keys.py` | | `IngestPipeline` | M05 | `services/rag/ingest.py` | | `IngestResult` | M05 | `services/rag/ingest.py` | | `InviteBlob` | M13 | `ui/onboarding.py` | | `JsonFormatter` | X03 | `observability/logging.py` | | `KeyPair` | M01 | `identity/keys.py` | | `LamportClock` | X02 | `events/lamport.py` | | `LlamaCppBackend` | M04 | `services/llm/backends/llama_cpp.py` | | `LlmBackend` | M04 | `services/llm/backends/base.py` | | `LlmBackendConfig` | X04 | `config.py` | | `LlmConfig` | X04 | `config.py` | | `LlmService` | M04 | `services/llm/service.py` | | `LmStudioBackend` | M04 | `services/llm/backends/lmstudio.py` | | `Location` | M06 | `services/marketplace/post.py` | | `ManifestPublisher` | M12 | `node.py` | | `MarketConfig` | X04 | `config.py` | | `MarketplaceService` | M06 | `services/marketplace/service.py` | | `MarketplaceView` | M06 | `services/marketplace/views.py` | | `MaterialisedView` | X02 | `events/replay.py` (Protocol) | | `MdnsAnnouncer` | M02 | `discovery/mdns.py` | | `MdnsBrowser` | M02 | `discovery/mdns.py` | | `Mode` | M09 | `emergency/state.py` (Literal alias) | | `NemotronBackend` *(new)* | M04 | `services/llm/backends/nemotron.py` | | `NodeManifest` | M01 | `identity/manifest.py` | | `ObservabilityConfig` | X04 | `config.py` | | `OllamaBackend` | M04 | `services/llm/backends/ollama.py` | | `OnboardingError` | M13 | `ui/onboarding.py` | | `OpenBmbBackend` *(new)* | M04 | `services/llm/backends/openbmb.py` | | `ParamsPredicate` | M03 | `bus/capability.py` (type alias) | | `PeerEvent` | M02 | `discovery/peers.py` | | `PeerRecord` | M02 | `discovery/peers.py` | | `PeerRegistry` | M02 | `discovery/peers.py` | | `PeriodicTask` | M12 | `node.py` | | `PinnedCerts` | X01 | `transport/tls.py` | | `Post` | M06 | `services/marketplace/post.py` | | `Profile` | (types) | `hearthnet/types.py` (Literal alias) | | `PubSubServer` | X01 | `transport/server.py` | | `RagConfig` | X04 | `config.py` | | `RagService` | M05 | `services/rag/service.py` | | `RateCheck` | X01 | `transport/__init__.py` | | `RateLimiter` | X01 | `transport/__init__.py` | | `RateLimitedLogger` | X03 | `observability/logging.py` | | `Registry` | M03 | `bus/registry.py` | | `RegistryEvent` | M03 | `bus/registry.py` | | `ReplayEngine` | X02 | `events/replay.py` | | `RevokedEntry` | M01 | `identity/manifest.py` | | `RouteRequest` | M03 | `bus/capability.py` | | `Router` | M03 | `bus/router.py` | | `SchemaValidator` | M03 | `bus/schema.py` | | `ScoredChunk` | M05 | `services/rag/store.py` | | `SentenceTransformerBackend` | M11 | `services/embedding/backends.py` | | `Service` | M03 | `services/base.py` (Protocol) | | `Snapshot` | X02 | `events/snapshot.py` | | `SnapshotStore` | X02 | `events/snapshot.py` | | `Span` | X03 | `observability/tracing.py` | | `SseReader` | X01 | `transport/streams.py` | | `SseWriter` | X01 | `transport/streams.py` | | `StateBus` | M09 | `emergency/state.py` | | `SyncClient` | X02 | `events/sync.py` | | `SyncResult` | X02 | `events/sync.py` | | `SyncServer` | X02 | `events/sync.py` | | `Token` | M04 | `services/llm/backends/base.py` | | `TopologyComponent` | M08 | `ui/topology.py` | | `TopologySnapshot` | M03 | `bus/__init__.py` | | `Trace` | X03 | `observability/tracing.py` | | `TraceHook` | M03 | `bus/trace.py` | | `TrackioExporter` *(new)* | X03 | `observability/metrics.py` | | `TransferManager` | M07 | `blobs/transfer.py` | | `TransportConfig` | X04 | `config.py` | | `UdpAnnouncer` | M02 | `discovery/udp.py` | | `UdpListener` | M02 | `discovery/udp.py` | | `UiApp` | M08 | `ui/app.py` | | `UiConfig` | X04 | `config.py` | --- ## 19. Capability → handler index For each capability in [CONTRACT §3.2](CAPABILITY_CONTRACT.md), where the handler lives: | Capability | Service | Handler | Trust | |------------|---------|---------|-------| | `llm.chat@1.0` | M04 `LlmService` | `handle_chat` | member | | `llm.complete@1.0` | M04 `LlmService` | `handle_complete` | member | | `embed.text@1.0` | M11 `EmbeddingService` | `handle_embed_text` | member | | `rag.query@1.0` | M05 `RagService` | `handle_query` | member | | `rag.ingest@1.0` | M05 `RagService` | `handle_ingest` | trusted | | `rag.list_corpora@1.0` | M05 `RagService` | `handle_list_corpora` | member | | `file.read@1.0` | M07 `FileService` | `handle_read` | member | | `file.list@1.0` | M07 `FileService` | `handle_list` | member | | `file.advertise@1.0` | M07 `FileService` | `handle_advertise` | member | | `file.put@1.0` | M07 `FileService` | `handle_put` | trusted | | `market.list@1.0` | M06 `MarketplaceService` | `handle_list` | member | | `market.post@1.0` | M06 `MarketplaceService` | `handle_post` | member | | `market.expire@1.0` | M06 `MarketplaceService` | `handle_expire` | member | | `market.search@1.0` | M06 `MarketplaceService` | `handle_search` | member | | `chat.send@1.0` | M10 `ChatService` | `handle_send` | member | | `chat.history@1.0` | M10 `ChatService` | `handle_history` | self | | `community.invite@1.0` | M13 (handler via bus from `make_invite`) | n/a | member with `can_invite` | | `community.revoke@1.0` | M13 / M01 helper | n/a | 3 trusted signatures | --- ## 20. Event-type → producer/consumer index For each [CONTRACT §7.2](CAPABILITY_CONTRACT.md) event type: | Event type | Produced by | View(s) consuming | |------------|-------------|--------------------| | `community.created` | M13 `create_community` | M01 community manifest builder | | `community.member.invited` | M13 `make_invite` | M01 | | `community.member.joined` | M13 `redeem_invite` | M01 | | `community.member.revoked` | M01 helper / `community.revoke` handler | M01 | | `community.member.promoted` / `.demoted` | M01 helpers | M01 | | `community.policy.updated` | M01 (root key only) | M01 | | `node.manifest.updated` | M12 `ManifestPublisher` | optional audit views | | `market.post.created` | M06 `handle_post` | M06 `MarketplaceView` | | `market.post.updated` | M06 (author only) | M06 `MarketplaceView` | | `market.post.expired` | M06 (author or sweeper) | M06 `MarketplaceView` | | `chat.message.sent` | M10 `handle_send` | M10 `ChatView` | | `chat.message.delivered` | M10 `DeliveryManager` | M10 `ChatView` | | `chat.message.read` | M10 (UI) | M10 `ChatView` | | `file.cid.advertised` | M07 `TransferManager.advertise` | local source index in `FileService` | | `file.cid.unpinned` | M07 `BlobStore.unpin` | local source index | | `rag.document.ingested` | M05 `IngestPipeline` | M05 (replicas may pre-fetch) | | `federation.peer.added` / `.removed` | reserved (Phase 2) | — | --- ## 21. Standard params for each capability descriptor Used by [CONTRACT §6.1](CAPABILITY_CONTRACT.md) node manifest embedding and by the bus's params-compatibility check. | Capability | `params` keys | |------------|---------------| | `llm.chat` | `model`, `quant`, `ctx`, `backend`, `modalities`, optionally `requires_internet` | | `llm.complete` | same as `llm.chat` | | `embed.text` | `model` | | `rag.query` | `corpus`, `embedding_model`, `k_max` | | `rag.ingest` | `corpora_available` (list) | | `rag.list_corpora` | `{}` | | `file.read` | `{}` | | `file.list` | `{}` | | `file.advertise` | `{}` | | `file.put` | `{}` | | `market.*` | `{}` | | `chat.send` | `{}` | | `chat.history` | `{}` | --- ## 22. Implementation checklist (one row per implementable unit) Tick these off as you build. Order: dependency-correct. ### X04 Config (~6 dataclasses, ~5 functions, ~1 exception) - [ ] `IdentityConfig`, `CommunityConfig`, `TransportConfig`, `DiscoveryConfig`, `BusConfig` - [ ] `LlmBackendConfig`, `LlmConfig` - [ ] `EmbeddingConfig`, `RagConfig`, `FileConfig`, `MarketConfig`, `ChatConfig` - [ ] `EmergencyConfig`, `UiConfig`, `ObservabilityConfig` *(incl. trackio_project/trackio_space)* - [ ] `Config` (aggregate) - [ ] `load`, `default_config`, `save`, `resolve_paths`, `validate` - [ ] `ConfigError` - [ ] `constants.py` with all 31 named constants ### X03 Observability (~6 classes, ~14 functions) - [ ] `configure`, `get_logger`, `JsonFormatter`, `RateLimitedLogger` - [ ] `configure` (metrics), `counter`, `histogram`, `gauge`, `disabled` - [ ] All 14 standard metrics pre-registered - [ ] `TrackioExporter` *(new, optional)* - [ ] `Trace`, `Span`, `new_trace`, `current_trace`, `attach`, `detach`, `span`, `get_recent` - [ ] `CheckResult`, `register`, `run_all`, `run_one` - [ ] 12 standard checks registered ### X02 Events (~7 classes, ~3 functions) - [ ] `EventType`, `Event` - [ ] `LamportClock` - [ ] `EventLog`, `EventLogError` - [ ] `MaterialisedView` (Protocol), `ReplayEngine` - [ ] `Snapshot`, `SnapshotStore`, `build_snapshot`, `restore_from_snapshot` - [ ] `HeadsReport`, `SyncResult`, `SyncClient`, `SyncServer` ### X01 Transport (~9 classes, ~1 exception) - [ ] `HttpServer` + 10 endpoints - [ ] `HttpClient`, `CallError` - [ ] `Frame`, `SseWriter`, `SseReader` - [ ] `FlowControl` - [ ] `PinnedCerts` - [ ] `RateCheck`, `RateLimiter` - [ ] `PubSubServer` ### M01 Identity (~12 classes, ~16 functions, ~1 exception) - [ ] `KeyPair`, all keys.py functions, `IdentityError` - [ ] `Endpoint`, `HardwareSpec`, `CapabilitySpec`, `NodeManifest` - [ ] `CommunityPolicy`, `CommunityMember`, `RevokedEntry`, `CommunityManifest` - [ ] All builder/parser/verifier functions - [ ] `CapabilityToken` stub ### M02 Discovery (~6 classes) - [ ] `PeerRecord`, `PeerEvent`, `PeerRegistry` - [ ] `MdnsAnnouncer`, `MdnsBrowser` - [ ] `UdpAnnouncer`, `UdpListener` - [ ] `DiscoveryError` ### M03 Capability Bus (~10 classes, ~1 function, ~1 exception) — CRITICAL - [ ] `CapabilityDescriptor`, `CapabilityEntry`, `RouteRequest` - [ ] `Diff`, `RegistryEvent`, `Registry` - [ ] `HealthTracker` - [ ] `SchemaValidator`, `compute_schema_hash` - [ ] `Router` (with scoring algorithm from M03 §5.4) - [ ] `CallTraceEvent`, `TraceHook` - [ ] `TopologySnapshot`, `CapabilityBus` (facade), `BusError` - [ ] `Service` Protocol in `services/base.py` ### M11 Embedding (~3 classes) - [ ] `EmbeddingBackend` Protocol - [ ] `SentenceTransformerBackend` - [ ] `EmbeddingService` + `handle_embed_text` + params predicate ### M04 LLM (~6 backends + 3 base classes + 2 base functions + 1 service) - [ ] `Token`, `ChatResult`, `BackendModel`, `LlmBackend` Protocol - [ ] `LlamaCppBackend` - [ ] `OllamaBackend` - [ ] `LmStudioBackend` - [ ] `HfApiBackend` - [ ] `AnthropicApiBackend` - [ ] **`NemotronBackend`** *(new — NVIDIA NIM / locally-hosted)* - [ ] **`OpenBmbBackend`** *(new — MiniCPM via vLLM/llama.cpp serve/SGLang)* - [ ] `count_tokens_approx`, `model_family` - [ ] `LlmService` + `handle_chat` + `handle_complete` + params predicate ### M05 RAG (~5 classes, ~4 functions) - [ ] `Chunk`, `chunk_text`, `chunk_pdf` - [ ] `ScoredChunk`, `CorpusStore`, `list_corpora`, `corpus_info` - [ ] `IngestResult`, `IngestPipeline` - [ ] `RagService` + 3 handlers + params predicate ### M07 File & Blobs (~4 classes, ~5 functions, ~1 exception) - [ ] `ChunkRef`, `BlobManifest` - [ ] All chunker.py functions - [ ] `BlobStore`, `BlobError` - [ ] `TransferManager` - [ ] `FileService` + 4 handlers ### M06 Marketplace (~3 classes) - [ ] `Location`, `Post` - [ ] `MarketplaceView` - [ ] `MarketplaceService` + 4 handlers + sweeper ### M10 Chat (~3 classes) - [ ] `ChatMessage`, `ChatView` - [ ] `DeliveryManager` - [ ] `ChatService` + 2 handlers ### M09 Emergency (~3 classes) - [ ] `EmergencyState`, `StateBus` - [ ] `Detector` (state machine + probe loop) ### M08 UI (~2 classes + ~6 tab builders + theme + mobile assets) - [ ] `UiApp`, `build_ui` - [ ] `TopologyComponent` - [ ] `hearthnet_theme`, `emergency_theme` - [ ] 6 tab builders - [ ] Mobile static assets ### M13 Onboarding (~1 class, ~7 functions, ~1 exception) - [ ] `InviteBlob` - [ ] All onboarding functions - [ ] `build_onboarding` - [ ] `OnboardingError` ### M12 CLI & Orchestrator (~17 commands + ~2 helper classes + 1 function) - [ ] All 17 Click subcommands - [ ] `ManifestPublisher`, `PeriodicTask` - [ ] `node.start()` — the 15-step composition --- ## 23. Notes on the trackio integration [Trackio](https://github.com/huggingface/trackio) is HuggingFace's local-first experiment tracker built on Gradio. Optional in HearthNet; enable by setting `config.observability.trackio_project`. Integration points: 1. **Activated by config.** `TrackioExporter` is constructed only if `trackio_project` is set. Otherwise the class is unused; HearthNet runs Prometheus-only. 2. **Bridged from TraceHook.** `M03 §3.6` `TraceHook.on_call_end` checks for an active exporter and forwards. No service code calls trackio directly. 3. **Optional HF Spaces sync.** If `trackio_space` is set, runs mirror to the named Space — handy for sharing demo telemetry. Off by default; the demo on Christof's machine logs locally. 4. **What gets logged.** Each LLM call is one step with: `latency_ms`, `tokens_in`, `tokens_out`, `model`, `backend`, `result`. Topology snapshots logged every 60s with mesh size, online state, capability counts. Marketplace post counts and chat throughput as gauges. 5. **Why this fits HearthNet.** Trackio is local-first (matches HearthNet's ethos), Gradio-native (matches the existing UI stack), and gives Christof a dashboard he already knows how to extend without adding Prometheus + Grafana. --- ## 24. Notes on the Nemotron and OpenBMB backends Both register exactly like existing backends: as `LlmBackend` implementations producing `BackendModel` entries that the service enumerates as `(backend, model)` capability instances. ### `NemotronBackend` NVIDIA's Nemotron family (Llama-3.1-Nemotron-70B, Nemotron-mini, Nemotron-4-340B-instruct). Two modes: - **Cloud (default):** `https://integrate.api.nvidia.com/v1`, OpenAI-compatible. `requires_internet=True`. Free tier exists; bring an `NVIDIA_API_KEY`. M09 will deregister this backend automatically when offline. - **Local (`local=True`):** point at a self-hosted NIM endpoint or vLLM-served Nemotron model. `requires_internet=False`. Models declared by `models: list[BackendModel]` at construction time. Use these typical entries: ```python BackendModel("nvidia/llama-3.1-nemotron-70b-instruct", quant="api", ctx_max=128000, modalities=["text"], requires_internet=True) BackendModel("nvidia/nemotron-mini-4b-instruct", quant="api", ctx_max=4096, modalities=["text"], requires_internet=True) ``` ### `OpenBmbBackend` OpenBMB's MiniCPM family — Christof's primary local-AI-workbench target. Typically served via vLLM, SGLang, or llama.cpp's HTTP server on `http://localhost:8000` (or wherever the workbench binds). OpenAI-compatible HTTP. `requires_internet=False` (always local). Models declared: ```python BackendModel("openbmb/MiniCPM4-8B", quant="fp16", ctx_max=32768, modalities=["text"], requires_internet=False) BackendModel("openbmb/MiniCPM-V-2_6", quant="fp16", ctx_max=8192, modalities=["text","vision"], requires_internet=False) ``` Vision-capable MiniCPM-V variant is reserved for Phase 2 when [CONTRACT §12 open question 1](CAPABILITY_CONTRACT.md) lifts; vision messages stay text-only in MVP. ### Config example ```toml [[llm.backends]] name = "openbmb" url = "http://localhost:8000" model = "openbmb/MiniCPM4-8B" [[llm.backends]] name = "nemotron" url = "https://integrate.api.nvidia.com/v1" model = "nvidia/llama-3.1-nemotron-70b-instruct" api_key_env = "NVIDIA_API_KEY" [[llm.backends]] name = "lmstudio" url = "http://192.168.188.25:1234" model = "qwen2.5-7b-instruct" ``` Three backends, four models if MiniCPM-V is later added → eight capability entries on the bus (two each for `llm.chat` and `llm.complete` × four models). The router picks among them at call time. --- ## 25. Coherence guarantees enforced by this reference If you implement strictly against this document, the following hold automatically: - **No symbol name appears in two different modules** — see §18. - **Every capability has exactly one handler** — see §19. - **Every event type has at least one producer** — see §20. - **Every constant is defined in `constants.py` and nowhere else** — see §0.2. - **The 15-step orchestration produces a runnable node** — see §17 `node.py`. - **Cross-references resolve** — every `M0N`/`X0N` link points at an existing spec; every spec section number used here exists. If you find a contradiction between this document and a spec, the spec wins by default — but file the discrepancy. The most common drift will be in field names of capability `params` (caught by the params predicate at registration time). --- ## 26. What is intentionally NOT in this document - **Test code** — see the `tests/` section in each spec. Implement after the production code compiles. - **Service-internal helpers** that are pure implementation detail (private functions inside one file with leading underscore). Add as you need them. - **Phase 2/3 modules** — `federation.*`, `ocr.*`, `tts.*`, `stt.*`, `trans.*`, `img.*`, `chat.thread.*`, `chat.forward.*` are mentioned in specs but have no MVP symbols. - **Vendor-specific tuning** — llama.cpp `n_threads`, vLLM tensor parallel, Nemotron prompt prefixes. Decide per backend at integration time. --- *End of HearthNet Implementation Reference.* *Spec set version: v1.0 · this document touched: 2026-06-09.*