feat(desktop): schema-driven memory-provider config surface

Make the desktop memory settings dynamic instead of hardcoded per
provider. The dropdown is now populated from discover_memory_providers()
(bundled + user-installed + pip) rather than a static enum, and each
provider's config panel is derived from its own get_config_schema() —
the same declaration `hermes memory setup` uses — so adding or porting a
provider is pure declaration with no bespoke UI, conditional, or
endpoint.

- memory_providers.py: reworked from a hand-written Hindsight registry
  into a pure adapter (describe_provider + coerce_value) that normalizes
  a provider's raw schema into typed fields — secret(+env), select,
  boolean, typed text — carrying `when` conditionals, url, and required.
- MemoryProvider ABC: add optional read_current_config() (default {}),
  the read-back mirror of save_config(). Ported mem0, hindsight, honcho,
  holographic; the rest fall back to schema defaults safely.
- web_server GET/PUT /api/memory/providers/{name}/config now load the
  live provider, derive its schema, write non-secrets via the provider's
  own save_config() (each keeps its native storage), persist secrets to
  the env store, and `when`-gate validation so hidden fields aren't
  required or written. Secrets stay write-only (is_set only).
- ProviderConfigPanel: `when`-conditional visibility (handles Hindsight's
  mode-gated duplicate keys), boolean toggle, and credential url links.
  Dropdown driven by getMemoryStatus(); hardcoded enum removed.

Tests assert the mapping contract and endpoint behavior (schema
derivation, save-via-save_config, secret-never-returned, when-gating,
select rejection) against real bundled providers rather than a snapshot
of a hardcoded list.
This commit is contained in:
Brooklyn Nicholson 2026-06-18 17:16:13 -05:00
parent 03d9a95a74
commit e48d18c8e2
16 changed files with 770 additions and 291 deletions

View file

@ -692,6 +692,20 @@ class HindsightMemoryProvider(MemoryProvider):
from utils import atomic_json_write
atomic_json_write(config_path, existing, mode=0o600)
def read_current_config(self):
"""Best-effort read-back of stored config for the desktop config panel.
Hindsight's deepest configuration (mode-specific deps, nested bank
layout) is owned by ``post_setup`` / ``hermes memory setup``; the
panel round-trips the common flat fields (mode, api_url, timeouts,
retain/recall tuning) it persists via ``save_config``.
"""
try:
cfg = _load_config()
return cfg if isinstance(cfg, dict) else {}
except Exception:
return {}
def post_setup(self, hermes_home: str, config: dict) -> None:
"""Custom setup wizard — installs only the deps needed for the selected mode."""
import subprocess

View file

@ -155,6 +155,22 @@ class HolographicMemoryProvider(MemoryProvider):
{"key": "hrr_dim", "description": "HRR vector dimensions", "default": "1024"},
]
def read_current_config(self):
"""Read back values save_config() wrote under plugins.hermes-memory-store."""
from pathlib import Path
from hermes_constants import get_hermes_home
try:
import yaml
config_path = Path(get_hermes_home()) / "config.yaml"
if config_path.exists():
with open(config_path, encoding="utf-8-sig") as f:
existing = yaml.safe_load(f) or {}
values = (existing.get("plugins") or {}).get("hermes-memory-store")
return values if isinstance(values, dict) else {}
except Exception:
pass
return {}
def initialize(self, session_id: str, **kwargs) -> None:
from hermes_constants import get_hermes_home
_hermes_home = str(get_hermes_home())

View file

@ -271,6 +271,20 @@ class HonchoMemoryProvider(MemoryProvider):
{"key": "baseUrl", "description": "Honcho base URL (for self-hosted)"},
]
def read_current_config(self):
"""Read back the non-secret values save_config() wrote to honcho.json."""
import json
from pathlib import Path
from hermes_constants import get_hermes_home
try:
path = Path(get_hermes_home()) / "honcho.json"
if path.exists():
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
pass
return {}
def post_setup(self, hermes_home: str, config: dict) -> None:
"""Run the full Honcho setup wizard after provider selection."""
import types

View file

@ -166,6 +166,12 @@ class Mem0MemoryProvider(MemoryProvider):
{"key": "rerank", "description": "Enable reranking for recall", "default": "true", "choices": ["true", "false"]},
]
def read_current_config(self):
try:
return _load_config()
except Exception:
return {}
def _get_client(self):
"""Thread-safe client accessor with lazy initialization."""
with self._client_lock: