fix(honcho): harden self-hosted setup paths

Self-hosted Honcho setup had four sharp edges:

- local/cloud URLs ending in /vN double-prefixed by the SDK (/v3/v3/... 404)
- authenticated local servers had no setup prompt for a JWT/bearer token
- profile-derived host keys could be dot-containing workspace IDs Honcho rejects
- memory-provider config files with API keys written world-readable per umask

This keeps existing behavior but makes those paths safer:

- strip a trailing /vN version segment from any configured baseUrl before SDK
  init (the SDK's route builders always prepend their own version prefix);
  auth-skipping stays loopback-only
- add an optional local JWT/bearer prompt in honcho setup, stored under
  hosts.<host>.apiKey
- derive new profile host keys with underscores, still reading legacy
  hermes.<profile> blocks
- write memory-provider config files atomically with 0600 via a shared
  utils.atomic_json_write(mode=) arg (honcho/hindsight/mem0/supermemory)
- skip honcho.json parsing in gateway cache-busting unless Honcho is the active
  memory provider; memoize by honcho.json mtime when active
- bust the gateway agent cache on memory.provider change
- add a hermes memory setup <provider> one-liner so fresh installs can configure
  a named provider without the picker (the per-provider hermes <provider>
  subcommand only registers once that provider is active)

Closes #20688, #29885, #26459, #30246, #33382, #32244.

Co-authored-by: BROCCOLO1D
This commit is contained in:
Erosika
2026-05-30 10:54:53 +05:30
committed by kshitij
parent aa32edcac5
commit 827ce602db
25 changed files with 734 additions and 101 deletions

View File

@ -87,6 +87,7 @@ def atomic_json_write(
data: Any,
*,
indent: int = 2,
mode: int | None = None,
**dump_kwargs: Any,
) -> None:
"""Write JSON data to a file atomically.
@ -99,13 +100,16 @@ def atomic_json_write(
path: Target file path (will be created or overwritten).
data: JSON-serializable data to write.
indent: JSON indentation (default 2).
mode: Optional final permission mode. When set, the temp file is
created and replaced with this mode, avoiding chmod-after-write
TOCTOU exposure for secret-bearing files.
**dump_kwargs: Additional keyword args forwarded to json.dump(), such
as default=str for non-native types.
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
original_mode = _preserve_file_mode(path)
original_mode = None if mode is not None else _preserve_file_mode(path)
fd, tmp_path = tempfile.mkstemp(
dir=str(path.parent),
@ -113,6 +117,8 @@ def atomic_json_write(
suffix=".tmp",
)
try:
if mode is not None:
os.fchmod(fd, mode)
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(
data,
@ -125,7 +131,13 @@ def atomic_json_write(
os.fsync(f.fileno())
# Preserve symlinks — swap in-place on the real file (GitHub #16743).
real_path = atomic_replace(tmp_path, path)
_restore_file_mode(real_path, original_mode)
if mode is not None:
try:
os.chmod(real_path, mode)
except OSError:
pass
else:
_restore_file_mode(Path(real_path), original_mode)
except BaseException:
# Intentionally catch BaseException so temp-file cleanup still runs for
# KeyboardInterrupt/SystemExit before re-raising the original signal.