feat(gateway): hermes gateway migrate --multiplex / --standalone

Moves a per-profile-gateway install onto one multiplexed default gateway:
table-driven preflight (duplicate credential via the gateway's own
fingerprint; secondary port-binders without a /p/<profile>/ ingress),
--dry-run, apply (stop + uninstall each secondary's service, record it in
<default>/gateway_migration.json, flip gateway.multiplex_profiles through
the config API, restart/install the default on the same service manager,
verify served_profiles), and --standalone rollback from the manifest.
Idempotent; refuses cleanly when already multiplexed or blocked.

`hermes profile create` points at `hermes gateway restart` when a live
multiplexer is detected (the served set is snapshotted at startup).
This commit is contained in:
Teknium
2026-09-12 00:50:51 -07:00
parent bcdb49ac7b
commit e2fc493427
3 changed files with 25 additions and 2 deletions
+6 -1
View File
@@ -6342,10 +6342,15 @@ def _cmd_migrate_legacy(args):
remove_legacy_hermes_units(interactive=not yes, dry_run=dry_run)
def _cmd_migrate(args):
from hermes_cli.gateway_migrate import cmd_migrate
cmd_migrate(args)
_GATEWAY_SUBCOMMANDS = {
None: _cmd_run, "run": _cmd_run, "setup": _cmd_setup, "install": _cmd_install,
"uninstall": _cmd_uninstall, "start": _cmd_start, "stop": _cmd_stop, "restart": _cmd_restart,
"status": _cmd_status, "list": _cmd_list, "migrate-legacy": _cmd_migrate_legacy,
"status": _cmd_status, "list": _cmd_list, "migrate-legacy": _cmd_migrate_legacy, "migrate": _cmd_migrate,
}
+6 -1
View File
@@ -208,7 +208,12 @@ def _profile_create(args):
print("\nNext steps:")
print(f" {name} setup Configure API keys and model")
print(f" {name} chat Start chatting")
print(f" {name} gateway start Start the messaging gateway")
from hermes_cli.gateway_multiplex_served import live_default_gateway_pid, recorded_served_profiles
if live_default_gateway_pid() is not None and recorded_served_profiles() is not None:
# The multiplexer snapshots the profile set at startup: a new profile is served only after a restart.
print(" hermes gateway restart Serve this profile from the running multiplexed gateway")
else:
print(f" {name} gateway start Start the messaging gateway")
if clone or clone_all:
print(f"\n Edit {profile_dir_display}/.env for different API keys")
print(f" Edit {profile_dir_display}/SOUL.md for different personality")
+13
View File
@@ -129,6 +129,19 @@ def build_gateway_parser(
help="List what would be removed without doing it")
_flag(gateway_migrate_legacy, "-y", "--yes", dest="yes", help="Skip the confirmation prompt")
gateway_migrate = gateway_subparsers.add_parser(
"migrate", help="Move per-profile gateways onto one multiplexed default gateway (or back)",
description="Stop and uninstall each secondary profile's standalone gateway, turn on "
"gateway.multiplex_profiles on the default profile and restart its gateway so it serves "
"every profile. Runs a preflight first (duplicate bot tokens, port-binding platforms "
"without a /p/<profile>/ ingress) and changes nothing when blocked. "
"--standalone rolls the recorded migration back.")
mode = gateway_migrate.add_mutually_exclusive_group()
_flag(mode, "--multiplex", dest="multiplex", help="Migrate to one multiplexed gateway (default)")
_flag(mode, "--standalone", dest="standalone", help="Roll back to per-profile gateways from the recorded manifest")
_flag(gateway_migrate, "--dry-run", dest="dry_run", help="Print the plan and blockers without changing anything")
_flag(gateway_migrate, "-y", "--yes", dest="yes", help="Apply without confirmation")
# enroll: redeem a single-use connector token for the per-gateway secret + per-tenant
# delivery key, written to .env. See docs/relay-connector-contract.md. EXPERIMENTAL.
gateway_enroll = gateway_subparsers.add_parser("enroll",