Files
hermes-agent/tui_gateway/methods_onboarding.py
Siddharth Balyan 3c6c132366 Setup profile is minted by the backend and found by role, not by name (#119456)
* feat: setup profile is minted by the backend and found by role, not by name

The guided onboarding runs in a profile the desktop used to create itself
(profiles.create with a soul, "already exists" treated as success) and
recognise by the literal "hermes-setup". The upcoming setup toolset grants
catalog installs to that profile, so the marker that grants it must be
written only by the backend.

- profile.yaml carries `role: setup`; read_profile_meta / write_profile_meta /
  ProfileInfo know it; profiles.list and GET /api/profiles report it.
- hermes_cli/setup_profile.py: ensure (find by role, adopt a pre-role
  hermes-setup dir, else clone default + soul + role) and reset (soul,
  memories, skills back to the created state, in place). The soul text moves
  here from the renderer.
- tui_gateway/methods_onboarding.py: onboarding.ensure_setup_profile and
  onboarding.reset_setup_profile. Neither takes a name; profiles.create and
  profiles.configure already reject `role` (unknown key, 4000).
- Copies never inherit the role: --clone-all, profile import, and a
  distribution that ships profile.yaml drop it.
- setup.status for a named profile reports `ready` once the boot bootstrap
  settled. Since one host backend serves every profile (#118246) the
  desktop's setup-profile probe lands on this branch, which never set
  `ready`, and the kickoff waited forever.
- Desktop: SETUP_PROFILE, ensureSetupProfile(profiles.create) and
  composeSetupSoul are gone. store/setup-profile.ts holds the name the
  backend returned (or the roster's role row after a relaunch); kickoff,
  handoff and the build card use it. The dev reset calls the reset RPC.

* fix: write the setup soul as bytes so Windows keeps \n line endings

* refactor(desktop): drop the renderer's setup-profile store; the backend is the only owner

Kickoff reads the name straight from onboarding.ensure_setup_profile and records it on
$setupSession, which every later step already carries. The handoff recovery check reads
the roster row's role. No renderer module holds a setup-profile name or a fallback lookup.
2026-09-23 01:25:04 +05:30

60 lines
2.4 KiB
Python

"""Onboarding JSON-RPC handlers: the backend owns the setup profile (``hermes_cli.setup_profile``).
Bodies are rebound onto server.py's globals (method_ctx.bind_module) and reference them bare.
"""
from .method_ctx import HandlerRegistry, bind_module
_registry = HandlerRegistry()
method = _registry.method
@method("onboarding.ensure_setup_profile")
def _(rid, params: dict) -> dict:
"""Create-or-read the setup profile. Takes no name: the backend picks it and finds it by role."""
from hermes_cli.setup_profile import ensure_setup_profile
try:
setup = ensure_setup_profile()
if setup.created:
# Same credential mirroring profiles.create gives the desktop's clones; auth.json stays
# shared with the root so a token refresh never forks.
_mirror_launch_credentials(setup.path, {"share_auth": True})
except Exception as e:
return _err(rid, 5073, str(e))
return _ok(rid, {"name": setup.name, "path": str(setup.path), "created": setup.created, "role": "setup"})
@method("onboarding.reset_setup_profile")
def _(rid, params: dict) -> dict:
"""Restore the setup profile to its created state in place; clears its session history."""
from hermes_cli.setup_profile import find_setup_profile, reset_setup_profile
found = find_setup_profile()
if found is None:
return _err(rid, 4072, "no setup profile to reset")
_clear_setup_sessions(found[1])
try:
setup = reset_setup_profile()
except Exception as e:
return _err(rid, 5074, str(e))
return _ok(rid, {"name": setup.name, "path": str(setup.path), "reset": True})
def _clear_setup_sessions(profile_dir) -> None:
"""Close this process's live sessions in the setup profile, then delete its stored sessions."""
target = Path(profile_dir).resolve()
with _sessions_lock:
live = [sid for sid, sess in _sessions.items()
if Path(sess.get("profile_home") or _hermes_home).resolve() == target]
for sid in live:
_close_session_by_id(sid, end_reason="setup_reset")
from hermes_state_registry import acquire, release_or_close
db = acquire(target / "state.db")
try:
ids = [row[0] for row in db._read_all("SELECT id FROM sessions")]
db.delete_sessions(ids, sessions_dir=target / "sessions")
finally:
release_or_close(db)
def register(server) -> None:
bind_module(globals(), server, skip=("_",))