HearthNet-Nemotron / docs /X04-config.md
Chris4K's picture
prd splitted + contracts
6f9a5fd
|
Raw
History Blame
7.42 kB

X04 — Configuration

Spec version: v1.0 Depends on: stdlib only Depended on by: every module


1. Responsibility

Single source of runtime configuration. Loads from disk, validates, and exposes a typed Config object. No module reads environment variables, files, or CLI flags directly — they all read from a Config instance handed to them.


2. File layout

hearthnet/
├── config.py        # implementation
└── constants.py     # immutable numeric constants (from GLOSSARY.md §Defaults)

config.toml lives at <CONFIG>/config.toml (see GLOSSARY.md for path resolution).


3. The Config object

# hearthnet/config.py
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

@dataclass(frozen=True)
class IdentityConfig:
    """Where keys live, and whether to auto-generate if missing."""
    keys_dir:     Path                        # <DATA>/keys
    auto_generate: bool = True

@dataclass(frozen=True)
class CommunityConfig:
    """Which community this node belongs to, and where its state lives."""
    community_id: Optional[str] = None        # None → must run `hearthnet init`
    state_dir:    Path = Path()               # <DATA>/communities/<id>

@dataclass(frozen=True)
class TransportConfig:
    host: str = "0.0.0.0"
    port: int = 7080
    tls_cert: Optional[Path] = None           # None → self-signed
    tls_key:  Optional[Path] = None

@dataclass(frozen=True)
class DiscoveryConfig:
    mdns_enabled: bool = True
    udp_enabled:  bool = True
    udp_multicast_group: str = "239.255.42.42"
    udp_port: int = 42424
    relay_urls:   list[str] = field(default_factory=list)   # Phase 2

@dataclass(frozen=True)
class BusConfig:
    prefer_local: bool = True
    local_load_threshold: float = 0.80

@dataclass(frozen=True)
class LlmBackendConfig:
    name:  str                                # "llama_cpp" | "ollama" | "lmstudio" | "hf_api" | "anthropic_api"
    url:   Optional[str] = None
    model: Optional[str] = None
    api_key_env: Optional[str] = None         # env var name; the actual key is never stored in config

@dataclass(frozen=True)
class LlmConfig:
    backends: list[LlmBackendConfig] = field(default_factory=list)

@dataclass(frozen=True)
class EmbeddingConfig:
    model: str = "BAAI/bge-small-en-v1.5"
    device: str = "auto"                      # "cpu" | "cuda" | "auto"

@dataclass(frozen=True)
class RagConfig:
    enabled: bool = True
    corpora_dir: Path = Path()                # <CACHE>/embeddings

@dataclass(frozen=True)
class FileConfig:
    blobs_dir: Path = Path()                  # <DATA>/blobs
    gc_threshold: float = 0.80

@dataclass(frozen=True)
class MarketConfig:
    enabled: bool = True
    default_ttl_seconds: int = 86400 * 7      # 7 days
    max_ttl_seconds:     int = 86400 * 30

@dataclass(frozen=True)
class ChatConfig:
    enabled: bool = True
    store_and_forward: bool = True

@dataclass(frozen=True)
class EmergencyConfig:
    probe_targets: list[str] = field(default_factory=lambda: [
        "1.1.1.1", "8.8.8.8", "cloudflare.com", "quad9.net"
    ])

@dataclass(frozen=True)
class UiConfig:
    host: str = "127.0.0.1"
    port: int = 7860
    launch_browser: bool = True

@dataclass(frozen=True)
class ObservabilityConfig:
    log_level: str = "info"
    log_dir:   Path = Path()                  # <LOG>
    metrics_enabled: bool = True
    otlp_endpoint: Optional[str] = None       # Phase 2

@dataclass(frozen=True)
class Config:
    identity:      IdentityConfig
    community:     CommunityConfig
    transport:     TransportConfig
    discovery:     DiscoveryConfig
    bus:           BusConfig
    llm:           LlmConfig
    embedding:     EmbeddingConfig
    rag:           RagConfig
    file:          FileConfig
    market:        MarketConfig
    chat:          ChatConfig
    emergency:     EmergencyConfig
    ui:            UiConfig
    observability: ObservabilityConfig

4. Public API

load(path: Path | None = None) -> Config

Loads from path if given, otherwise from the platform-standard location. Applies defaults for omitted sections. Validates and returns a frozen Config.

Raises:

  • ConfigError("invalid_toml") — TOML parse failure
  • ConfigError("invalid_field", field=...) — type or value validation
  • ConfigError("path_resolution") — XDG resolution failed (e.g. read-only filesystem)

default_config() -> Config

Returns a Config populated entirely from defaults. Used by tests and hearthnet init.

save(config: Config, path: Path | None = None) -> None

Serialises a Config to TOML and writes atomically (write to tempfile, rename). Used by hearthnet init.

resolve_paths(config: Config) -> Config

Resolves empty Path() fields to their canonical XDG locations. Called by load() automatically. Idempotent.

validate(config: Config) -> None

Cross-field validation (e.g. transport port not equal to udp port). Raises ConfigError on failure. Called by load().

ConfigError(Exception)

class ConfigError(Exception):
    def __init__(self, code: str, **details):
        self.code = code
        self.details = details
        super().__init__(f"{code}: {details}")

5. Default config.toml

[identity]
auto_generate = true

[community]
# community_id is set by `hearthnet init`

[transport]
host = "0.0.0.0"
port = 7080

[discovery]
mdns_enabled = true
udp_enabled  = true

[bus]
prefer_local = true
local_load_threshold = 0.8

[[llm.backends]]
name  = "lmstudio"
url   = "http://192.168.188.25:1234"
model = "qwen2.5-7b-instruct"

[embedding]
model  = "BAAI/bge-small-en-v1.5"
device = "auto"

[rag]
enabled = true

[file]
gc_threshold = 0.8

[market]
enabled = true
default_ttl_seconds = 604800
max_ttl_seconds = 2592000

[chat]
enabled = true
store_and_forward = true

[ui]
host = "127.0.0.1"
port = 7860
launch_browser = true

[observability]
log_level = "info"
metrics_enabled = true

6. Cross-cutting constants

hearthnet/constants.py holds the values from GLOSSARY.md §Defaults. These are NOT configurable. Examples:

MANIFEST_TTL_SECONDS                   = 30
MANIFEST_REPUBLISH_INTERVAL_SECONDS    = 20
EMERGENCY_PROBE_INTERVAL_ONLINE        = 10
EMERGENCY_PROBE_INTERVAL_OFFLINE       = 2
STREAM_WINDOW_FRAMES                   = 16
CHUNK_SIZE_BYTES                       = 262144
HEALTH_WINDOW_CALLS                    = 100
HEALTH_QUARANTINE_THRESHOLD            = 0.5
HEALTH_QUARANTINE_SECONDS              = 60
# ... see GLOSSARY.md for the complete list

Rationale for non-configurability: these affect interop. A node tweaking MANIFEST_TTL_SECONDS will desync from the network.


7. Tests

  • test_default_config_round_tripssave(default_config()); load() returns equal config
  • test_invalid_toml_raises — malformed TOML → ConfigError("invalid_toml")
  • test_missing_required_field_raises — community section without community_id is OK (post-init); but other validations apply
  • test_path_resolution_xdg — empty paths resolve to user_data_dir, etc.
  • test_env_var_substitution${ENV_VAR} in TOML strings is expanded by load()

8. References

  • Constants list: GLOSSARY.md §Defaults
  • Used by all modules; this is the universal entry point