fix(honcho): scope config writes to hosts.hermes, not root

Config writes from hermes honcho setup/peer now go to
hosts.hermes instead of mutating root-level keys. Root is
reserved for the user or honcho CLI. apiKey remains at root
as a shared credential.

Reads updated to check hosts.hermes first with root fallback
for all fields (peerName, enabled, saveMessages, environment,
sessionStrategy, sessionPeerPrefix).
This commit is contained in:
Erosika
2026-03-11 17:45:35 -04:00
parent d987ff54a1
commit 3c813535a7
2 changed files with 64 additions and 39 deletions

View File

@@ -88,7 +88,12 @@ def cmd_setup(args) -> None:
if not _ensure_sdk_installed():
return
# API key
# All writes go to hosts.hermes — root keys are managed by the user
# or the honcho CLI only.
hosts = cfg.setdefault("hosts", {})
hermes_host = hosts.setdefault(HOST, {})
# API key — shared credential, lives at root so all hosts can read it
current_key = cfg.get("apiKey", "")
masked = f"...{current_key[-8:]}" if len(current_key) > 8 else ("set" if current_key else "not set")
print(f" Current API key: {masked}")
@@ -96,45 +101,39 @@ def cmd_setup(args) -> None:
if new_key:
cfg["apiKey"] = new_key
if not cfg.get("apiKey"):
effective_key = cfg.get("apiKey", "")
if not effective_key:
print("\n No API key configured. Get your API key at https://app.honcho.dev")
print(" Run 'hermes honcho setup' again once you have a key.\n")
return
# Peer name
current_peer = cfg.get("peerName", "")
current_peer = hermes_host.get("peerName") or cfg.get("peerName", "")
new_peer = _prompt("Your name (user peer)", default=current_peer or os.getenv("USER", "user"))
if new_peer:
cfg["peerName"] = new_peer
# Host block
hosts = cfg.setdefault("hosts", {})
hermes_host = hosts.setdefault(HOST, {})
hermes_host["peerName"] = new_peer
current_workspace = hermes_host.get("workspace") or cfg.get("workspace", "hermes")
new_workspace = _prompt("Workspace ID", default=current_workspace)
if new_workspace:
hermes_host["workspace"] = new_workspace
# Also update flat workspace if it was the primary one
if cfg.get("workspace") == current_workspace:
cfg["workspace"] = new_workspace
hermes_host.setdefault("aiPeer", HOST)
# Memory mode
current_mode = cfg.get("memoryMode", "hybrid")
current_mode = hermes_host.get("memoryMode") or cfg.get("memoryMode", "hybrid")
print(f"\n Memory mode options:")
print(" hybrid — write to both Honcho and local MEMORY.md (default)")
print(" honcho — Honcho only, skip MEMORY.md writes")
print(" local — MEMORY.md only, Honcho disabled")
new_mode = _prompt("Memory mode", default=current_mode)
if new_mode in ("hybrid", "honcho", "local"):
cfg["memoryMode"] = new_mode
hermes_host["memoryMode"] = new_mode
else:
cfg["memoryMode"] = "hybrid"
hermes_host["memoryMode"] = "hybrid"
# Write frequency
current_wf = str(cfg.get("writeFrequency", "async"))
current_wf = str(hermes_host.get("writeFrequency") or cfg.get("writeFrequency", "async"))
print(f"\n Write frequency options:")
print(" async — background thread, no token cost (recommended)")
print(" turn — sync write after every turn")
@@ -142,22 +141,22 @@ def cmd_setup(args) -> None:
print(" N — write every N turns (e.g. 5)")
new_wf = _prompt("Write frequency", default=current_wf)
try:
cfg["writeFrequency"] = int(new_wf)
hermes_host["writeFrequency"] = int(new_wf)
except (ValueError, TypeError):
cfg["writeFrequency"] = new_wf if new_wf in ("async", "turn", "session") else "async"
hermes_host["writeFrequency"] = new_wf if new_wf in ("async", "turn", "session") else "async"
# Recall mode
current_recall = cfg.get("recallMode", "hybrid")
current_recall = hermes_host.get("recallMode") or cfg.get("recallMode", "hybrid")
print(f"\n Recall mode options:")
print(" hybrid — pre-warmed context + memory tools available (default)")
print(" context — pre-warmed context only, memory tools suppressed")
print(" tools — no pre-loaded context, rely on tool calls only")
new_recall = _prompt("Recall mode", default=current_recall)
if new_recall in ("hybrid", "context", "tools"):
cfg["recallMode"] = new_recall
hermes_host["recallMode"] = new_recall
# Session strategy
current_strat = cfg.get("sessionStrategy", "per-session")
current_strat = hermes_host.get("sessionStrategy") or cfg.get("sessionStrategy", "per-session")
print(f"\n Session strategy options:")
print(" per-session — new Honcho session each run, named by Hermes session ID (default)")
print(" per-directory — one session per working directory")
@@ -165,10 +164,10 @@ def cmd_setup(args) -> None:
print(" global — single session across all directories")
new_strat = _prompt("Session strategy", default=current_strat)
if new_strat in ("per-session", "per-repo", "per-directory", "global"):
cfg["sessionStrategy"] = new_strat
hermes_host["sessionStrategy"] = new_strat
cfg.setdefault("enabled", True)
cfg.setdefault("saveMessages", True)
hermes_host.setdefault("enabled", True)
hermes_host.setdefault("saveMessages", True)
_write_config(cfg)
print(f"\n Config written to {GLOBAL_CONFIG_PATH}")
@@ -321,7 +320,7 @@ def cmd_peer(args) -> None:
# Show current values
hosts = cfg.get("hosts", {})
hermes = hosts.get(HOST, {})
user = cfg.get('peerName') or '(not set)'
user = hermes.get('peerName') or cfg.get('peerName') or '(not set)'
ai = hermes.get('aiPeer') or cfg.get('aiPeer') or HOST
lvl = hermes.get("dialecticReasoningLevel") or cfg.get("dialecticReasoningLevel") or "low"
max_chars = hermes.get("dialecticMaxChars") or cfg.get("dialecticMaxChars") or 600
@@ -337,9 +336,9 @@ def cmd_peer(args) -> None:
return
if user_name is not None:
cfg["peerName"] = user_name.strip()
cfg.setdefault("hosts", {}).setdefault(HOST, {})["peerName"] = user_name.strip()
changed = True
print(f" User peer → {cfg['peerName']}")
print(f" User peer → {user_name.strip()}")
if ai_name is not None:
cfg.setdefault("hosts", {}).setdefault(HOST, {})["aiPeer"] = ai_name.strip()

View File

@@ -147,17 +147,28 @@ class HonchoClientConfig:
)
linked_hosts = host_block.get("linkedHosts", [])
api_key = raw.get("apiKey") or os.environ.get("HONCHO_API_KEY")
api_key = (
host_block.get("apiKey")
or raw.get("apiKey")
or os.environ.get("HONCHO_API_KEY")
)
environment = (
host_block.get("environment")
or raw.get("environment", "production")
)
# Auto-enable when API key is present (unless explicitly disabled)
# This matches user expectations: setting an API key should activate the feature.
explicit_enabled = raw.get("enabled")
if explicit_enabled is None:
# Not explicitly set in config -> auto-enable if API key exists
enabled = bool(api_key)
# Host-level enabled wins, then root-level, then auto-enable if key exists.
host_enabled = host_block.get("enabled")
root_enabled = raw.get("enabled")
if host_enabled is not None:
enabled = host_enabled
elif root_enabled is not None:
enabled = root_enabled
else:
# Respect explicit setting
enabled = explicit_enabled
# Not explicitly set anywhere -> auto-enable if API key exists
enabled = bool(api_key)
# write_frequency: accept int or string
raw_wf = (
@@ -170,16 +181,31 @@ class HonchoClientConfig:
except (TypeError, ValueError):
write_frequency = str(raw_wf)
# saveMessages: host wins (None-aware since False is valid)
host_save = host_block.get("saveMessages")
save_messages = host_save if host_save is not None else raw.get("saveMessages", True)
# sessionStrategy / sessionPeerPrefix: host first, root fallback
session_strategy = (
host_block.get("sessionStrategy")
or raw.get("sessionStrategy", "per-session")
)
host_prefix = host_block.get("sessionPeerPrefix")
session_peer_prefix = (
host_prefix if host_prefix is not None
else raw.get("sessionPeerPrefix", False)
)
return cls(
host=host,
workspace_id=workspace,
api_key=api_key,
environment=raw.get("environment", "production"),
peer_name=raw.get("peerName"),
environment=environment,
peer_name=host_block.get("peerName") or raw.get("peerName"),
ai_peer=ai_peer,
linked_hosts=linked_hosts,
enabled=enabled,
save_messages=raw.get("saveMessages", True),
save_messages=save_messages,
**_resolve_memory_mode(
raw.get("memoryMode", "hybrid"),
host_block.get("memoryMode"),
@@ -201,8 +227,8 @@ class HonchoClientConfig:
or raw.get("recallMode")
or "hybrid"
),
session_strategy=raw.get("sessionStrategy", "per-session"),
session_peer_prefix=raw.get("sessionPeerPrefix", False),
session_strategy=session_strategy,
session_peer_prefix=session_peer_prefix,
sessions=raw.get("sessions", {}),
raw=raw,
)