← Indice documentazione Microprogettazione › config

myclaw

config — schema, overrides, secrets
Microprogettazione v1.0 — 22 aprile 2026
Documento trasversale di fase 1.
Definisce cosa è configurabile, cosa non lo è, e come i segreti vengono gestiti.

Pubblico: chi implementerà il loader pydantic-settings e i comandi ops. Lettura: 18 min.

Indice

  1. Scopo e confini
  2. Configurabile vs non configurabile
  3. Layering delle sorgenti
  4. Schema pydantic-settings
  5. Secrets: dove vivono, come si leggono
  6. Hot reload
  7. CLI operativa
  8. Contratto Python
  9. Alternative considerate
  10. Test di conformità
  11. Riferimenti

1. Scopo e confini

config è il componente trasversale che legge, valida, espone la configurazione a tutto il resto del sistema. Definisce una unica fonte di verità tipata (Settings) costruita per layering da file TOML, variabili d'ambiente e secret store. Questo doc non è il luogo dei valori: è il luogo del meccanismo.

Cosa copre

Cosa non copre

2. Configurabile vs non configurabile

Tre categorie disgiunte. La distinzione è di design, non di comodo: spostare qualcosa da "non configurabile" a "configurabile" richiede un cambiamento nella Costituzione o un bump di versione del contratto.

CategoriaCosa contieneDove vive
Non configurabile Le quattro Leggi. Forbidden paths hard-coded. Algoritmi di firma. Struttura del prompt di sistema. Marker di boundary (<constitution>). SOUL.md + costanti nel codice sorgente.
Configurabile con versione Livelli di autonomy disponibili. Registro delle capability. Modelli LLM usati per tier. Policy di retention della memoria. workspace/IDENTITY.md + config.toml dichiarato.
Configurabile liberamente Tutto il resto: path, porte, timeout, soglie (TAU, EPSILON), credenziali, URL esterni. config.toml, env vars, secret store.

3. Layering delle sorgenti

La configurazione finale è costruita per merge a strati: ogni strato successivo può aggiungere o sovrascrivere valori degli strati precedenti, mai toglierli. La validazione tipata avviene dopo il merge.

1. Defaults hardcoded nel codice (Field(...)) sempre presenti precedenza: minima 2. config.toml /etc/myclaw/ config.toml versionato, audit-grade 3. local.toml ~/.config/myclaw/ local.toml override per-utente, gitignored 4. env MYCLAW_* in processo per run 5. secrets solo SecretStr da file 0600 mai in log precedenza: massima merge → validazione pydantic → Settings frozen
Figura 1 — I cinque strati di configurazione. Merge in ordine; ogni strato può sovrascrivere il precedente. La validazione avviene una sola volta, dopo il merge, sull'oggetto risultante.

4. Schema pydantic-settings

Lo schema è una gerarchia di BaseSettings annidati, con env_prefix e supporto TOML via pydantic-settings 2.x.

from pydantic import BaseModel, SecretStr, Field, HttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from pathlib import Path

class GatewaySettings(BaseModel):
    bind_host: str = "127.0.0.1"
    bind_port: int = Field(8810, ge=1024, le=65535)
    public_base_url: HttpUrl | None = None

class LLMProviderSettings(BaseModel):
    name: str                          # "anthropic" | "openai" | "local-vllm"
    api_key: SecretStr | None = None
    base_url: HttpUrl | None = None
    model: str                         # es. "claude-sonnet-4-6"
    max_tokens: int = 4096

class CostTierSettings(BaseModel):
    local_fast: LLMProviderSettings
    frontier:   LLMProviderSettings
    budget_soft_eur: float = 2.0
    budget_hard_eur: float = 5.0

class MemorySettings(BaseModel):
    workspace_path: Path = Path("~/.myclaw/workspace").expanduser()
    episodic_retention_days: int = 60
    semantic_budget_kb: int = 2
    reflection_cron: str = "0 3 * * *"
    tau_days: int = 30                 # Ebbinghaus decay

class SynapseSettings(BaseModel):
    retrieval_epsilon: float = Field(0.05, ge=0, le=0.5)
    strike_threshold: int = 3
    dormant_days: int = 90
    quarantine_grace_days: int = 30

class ObservabilitySettings(BaseModel):
    audit_path: Path = Path(".audit")
    log_level: str = "INFO"
    metrics_enabled: bool = True

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="MYCLAW_",
        env_nested_delimiter="__",
        toml_file=("/etc/myclaw/config.toml",
                   "~/.config/myclaw/local.toml"),
        secrets_dir="/run/secrets/myclaw",   # file-per-secret, 0600
        extra="forbid",
        frozen=True,
    )

    gateway: GatewaySettings = GatewaySettings()
    llm:     CostTierSettings
    memory:  MemorySettings = MemorySettings()
    synapse: SynapseSettings = SynapseSettings()
    obs:     ObservabilitySettings = ObservabilitySettings()

Regole di validazione non negoziabili

5. Secrets: dove vivono, come si leggono

Un secret non è una variabile di configurazione. I segreti abitano un proprio canale e vengono letti come SecretStr, il cui valore è accessibile solo con .get_secret_value(). Ogni logger normale li stampa come **********.

Sorgenti permesse (in ordine di preferenza)

  1. File-per-secret in /run/secrets/myclaw/, uno per chiave (es. llm__frontier__api_key), mode 0600, owner myclaw. Letti via secrets_dir di pydantic-settings. Preferita.
  2. Variabili d'ambiente MYCLAW_LLM__FRONTIER__API_KEY=.... Accettate ma l'environment è visibile in /proc/<pid>/environ: meno robusto.
  3. File TOML: NON permesso per secrets. Il loader rifiuta un SecretStr che venga popolato da config.toml/local.toml (ValueError all'avvio).

Regole di gestione

DECISIONE v1: nessun supporto a Vault / Bitwarden / AWS SM in v1. Un file 0600 in /run/secrets/ (tmpfs) è più che adeguato al contesto domestico. Rimandato senza rimpianti.

6. Hot reload

Non tutta la config può essere ricaricata a caldo senza rischio. La separo in tre classi:

ClasseEsempioPolitica
Hot-reloadablelog_level, soglie TAU/EPSILON, reflection_cronSIGHUP → ri-validate → swap atomico del Settings.
Restart-requiredbind_host/port, workspace_path, provider LLMModifica rilevata → warning in log; gli effetti si applicano al prossimo restart.
Non modificabile a runtimeForbidden paths, struttura del promptNon vivono qui: appartengono a constitution.

Il loader mantiene una classificazione per campo: un campo marcato restart_required=True che cambia tra due load non applica il nuovo valore ma logga la differenza come evento config.restart_pending.

7. CLI operativa

Una manciata di comandi per trasparenza e ops. Tutti rispettano la regola secrets-redatti.

ComandoFa
myclaw config showStampa la config effettiva con secrets redatti + provenance per ogni campo (da quale strato viene).
myclaw config verifyValida senza avviare il gateway. Exit code 0/1.
myclaw config diffMostra le differenze fra config attiva e quella che risulterebbe da un nuovo load.
myclaw config reloadEquivalente a SIGHUP: ri-load + applicazione dei soli campi hot-reloadable.
myclaw config schemaEmette JSON Schema corrispondente a Settings per editor / review.

8. Contratto Python

from typing import Protocol, Literal
from pathlib import Path

class ConfigLoader(Protocol):
    def load(self) -> "Settings":
        """Merge dei 5 strati + validazione. Solleva ValidationError con
        path del campo offensore."""
        ...

    def reload(self, signal: Literal["SIGHUP"] | None = None) -> "ConfigDiff":
        """Esegue load, confronta con la corrente, applica solo i campi
        hot-reloadable. Ritorna il diff."""
        ...

    def provenance(self, field_path: str) -> Literal["default", "toml", "local", "env", "secret"]:
        """Dice da quale strato viene l'attuale valore di un campo."""
        ...

@dataclass
class ConfigDiff:
    applied: list[str]              # campi hot-reloadable cambiati e applicati
    restart_pending: list[str]      # campi cambiati ma non applicati
    unchanged: int
    new_hash: str                   # sha256 del dump redacted

# Errori
class ConfigValidationError(Exception): ...
class SecretInTomlError(Exception): ...  # secret letto da TOML: rifiutato
class UnknownConfigKeyError(Exception): ...  # extra="forbid"

9. Alternative considerate

AlternativaPerché scartata
YAML invece di TOMLPiù flessibile ma più subdolo (indentazione, tipi impliciti). TOML è meno espressivo ma rende gli errori visibili nei diff.
Config fatta a mano con configparserNessun typing, nessuna validazione. Rifatta da zero ogni volta.
Dataclass + json.loadValidazione a mano, nessun supporto env/secrets. pydantic-settings è la scelta ergonomica.
Config mutabile a runtime via APIApre a modifiche non auditate. frozen=True impedisce deriva.
Tutto in env (12-factor stretto)Buono per container stateless, doloroso per home agent con decine di parametri. File + env è pragmatic.
Integrazione Vault/Bitwarden in v1Richiede infrastruttura, chiavi, rotazione automatica. Rimandato.
Reload "esplosivo" (swap di tutto a caldo)Rischioso: cambiare bind_port a caldo significa chiudere connessioni. La tabella di classe in §6 è la difesa.

10. Test di conformità

InvarianteTest
Merge a stratidefault + config.toml con bind_port=9000 + env MYCLAW_GATEWAY__BIND_PORT=9100 → valore effettivo 9100, provenance="env".
Kiave sconosciuta blocca avvioconfig.toml con [gateway] mistery=1UnknownConfigKeyError, gateway non parte.
Secret da TOML rifiutatollm.frontier.api_key = "..." in local.toml → SecretInTomlError.
Secret da file 0600File /run/secrets/myclaw/llm__frontier__api_key → popolato come SecretStr, get_secret_value() torna il contenuto.
SecretStr non in logSetup logger & dump config → stringa dei log non contiene il valore del secret (grep).
Frozensettings.gateway.bind_port = 9999FrozenInstanceError.
Hot reload campi compatibiliSIGHUP dopo change di obs.log_level → applied=["obs.log_level"], restart_pending=[].
Hot reload campi restart-requiredSIGHUP dopo change di gateway.bind_port → applied=[], restart_pending=["gateway.bind_port"], evento config.restart_pending.
ProvenancePer ogni campo, provenance(path) restituisce esattamente lo strato originante.
Config show redige secretsOutput di myclaw config show non contiene valori di SecretStr, solo **********.
Schema exportabilemyclaw config schema produce un JSON Schema valido (verifica con jsonschema stesso).
Diag redige secretsSnapshot diagnostico non contiene valori di SecretStr.

11. Riferimenti

RiferimentoCosa abbiamo preso
pydantic-settings 2.xLoader a strati (env, toml, secrets_dir), SecretStr, validazione tipata.
12-factor AppEnv per config, secrets in canale separato — adattato (non stretto) al contesto home.
TOML specFormato con diff leggibili, tipi espliciti, sezioni.
systemd LoadCredential=Meccanismo che popola /run/secrets/ in modo compatibile con pydantic-settings secrets_dir.
Constitution §4Giustificazione per cui Costituzione non passa da config.
PolicyMolte soglie tunabili di retrieval/quota abitano in config e vengono lette da policy.
Observability §auditEvento config.loaded / config.restart_pending come punto di verità.

Continua a leggere

gemello
constitution
Il non-configurabile: cosa non passa di qui e perché.
consumer
policy
Principale consumer di Settings: soglie, quota, budget.
audit
observability
Dove loggiamo config.loaded e config.restart_pending.
indice
Torna alla landing
Microprogettazione, tutti i doc.

myclaw — config microprogettazione v1.0 — 2026-04-22
Ultimo doc dell'ordine canonico. La tabella di microprogettazione è completa.