mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
feat(honcho-setup): add deployment-shape step to identity-mapping wizard
The PR #27371 resolver introduced three identity-mapping config keys (pinPeerName, userPeerAliases, runtimePeerPrefix), but operators had no guided way to set them — they had to read the README, understand the resolver ladder, and hand-edit honcho.json. This commit adds an interactive step to 'hermes honcho setup' that asks one question ('what's your deployment shape?') and writes the right combination of keys. Three shapes cover the realistic deployments: * single -- pinPeerName=true. All gateway users collapse to your peerName. Recommended for personal/single-operator use. * multi -- pinPeerName=false, no aliases. Each runtime user gets their own peer. Optional runtimePeerPrefix for cross- platform namespace isolation. * hybrid -- pinPeerName=false, with userPeerAliases mapping YOUR runtime IDs (Telegram UID, Discord snowflake, Slack user, Matrix MXID) to peerName. Multi-user gateway where you are a privileged operator. A 'skip' option leaves existing identity-mapping config untouched — critical because re-running setup must not silently wipe operator- curated aliases. The wizard detects the current shape from existing config so the prompt's default matches what the operator already has.
This commit is contained in:
parent
c03960decd
commit
0bac880991
2 changed files with 229 additions and 1 deletions
|
|
@ -441,6 +441,86 @@ def cmd_setup(args) -> None:
|
|||
if new_workspace:
|
||||
hermes_host["workspace"] = new_workspace
|
||||
|
||||
# --- 3b. Deployment shape ---
|
||||
# Determines how runtime user identities (Telegram UIDs, Discord
|
||||
# snowflakes, etc.) map to Honcho peers in gateway sessions. Three
|
||||
# shapes cover the realistic deployments; each writes a different
|
||||
# combination of pinPeerName / userPeerAliases / runtimePeerPrefix.
|
||||
# See plugins/memory/honcho/README.md for the resolver ladder.
|
||||
current_pin = bool(hermes_host.get("pinPeerName", False))
|
||||
current_aliases = hermes_host.get("userPeerAliases", {})
|
||||
current_prefix = hermes_host.get("runtimePeerPrefix", "")
|
||||
|
||||
if current_pin:
|
||||
current_shape = "single"
|
||||
elif current_aliases:
|
||||
current_shape = "hybrid"
|
||||
else:
|
||||
current_shape = "multi"
|
||||
|
||||
print("\n Deployment shape (how gateway users map to peers):")
|
||||
print(" single -- all platforms route to your peer (recommended for personal use)")
|
||||
print(" multi -- each platform user gets their own peer (multi-user bots)")
|
||||
print(" hybrid -- multi-user, but YOUR runtime IDs alias to your peer")
|
||||
print(" skip -- don't touch identity-mapping config")
|
||||
new_shape = _prompt("Deployment shape", default=current_shape).strip().lower()
|
||||
|
||||
if new_shape == "single":
|
||||
hermes_host["pinPeerName"] = True
|
||||
hermes_host.pop("userPeerAliases", None)
|
||||
hermes_host.pop("runtimePeerPrefix", None)
|
||||
print(f" pinPeerName=true → all gateway users route to '{hermes_host.get('peerName', '?')}'.")
|
||||
elif new_shape == "multi":
|
||||
hermes_host["pinPeerName"] = False
|
||||
# Preserve any existing operator-curated aliases / prefix.
|
||||
if "userPeerAliases" not in hermes_host:
|
||||
hermes_host["userPeerAliases"] = {}
|
||||
_prefix_default = current_prefix or ""
|
||||
_new_prefix = _prompt(
|
||||
"Runtime peer prefix (e.g. 'telegram_', blank for none)",
|
||||
default=_prefix_default,
|
||||
).strip()
|
||||
if _new_prefix:
|
||||
hermes_host["runtimePeerPrefix"] = _new_prefix
|
||||
else:
|
||||
hermes_host.pop("runtimePeerPrefix", None)
|
||||
print(" Multi-user mode: each runtime ID → own peer. Use 'hermes honcho status' to inspect.")
|
||||
elif new_shape == "hybrid":
|
||||
hermes_host["pinPeerName"] = False
|
||||
peer_target = hermes_host.get("peerName") or current_peer or "user"
|
||||
existing_aliases = dict(current_aliases) if isinstance(current_aliases, dict) else {}
|
||||
print(f"\n Add runtime IDs that should alias to peer '{peer_target}'.")
|
||||
print(" Leave blank to skip a platform. Existing aliases are preserved.")
|
||||
for platform_label, alias_hint in (
|
||||
("Telegram UID", "e.g. 86701400"),
|
||||
("Discord snowflake", "e.g. 491827364"),
|
||||
("Slack user ID", "e.g. U04ABCDEF"),
|
||||
("Matrix MXID", "e.g. @you:matrix.org"),
|
||||
):
|
||||
entered = _prompt(f" {platform_label} ({alias_hint})", default="").strip()
|
||||
if entered:
|
||||
existing_aliases[entered] = peer_target
|
||||
if existing_aliases:
|
||||
hermes_host["userPeerAliases"] = existing_aliases
|
||||
elif "userPeerAliases" in hermes_host:
|
||||
# No aliases entered and none pre-existing — leave the key absent.
|
||||
if not hermes_host["userPeerAliases"]:
|
||||
hermes_host.pop("userPeerAliases", None)
|
||||
_prefix_default = current_prefix or ""
|
||||
_new_prefix = _prompt(
|
||||
"Runtime peer prefix for unknown users (e.g. 'telegram_', blank for none)",
|
||||
default=_prefix_default,
|
||||
).strip()
|
||||
if _new_prefix:
|
||||
hermes_host["runtimePeerPrefix"] = _new_prefix
|
||||
else:
|
||||
hermes_host.pop("runtimePeerPrefix", None)
|
||||
print(f" Hybrid mode: your runtime IDs → '{peer_target}', others → own peer.")
|
||||
elif new_shape == "skip":
|
||||
pass # leave config untouched
|
||||
else:
|
||||
print(f" Unknown shape '{new_shape}' — leaving identity-mapping config untouched.")
|
||||
|
||||
# --- 4. Observation mode ---
|
||||
current_obs = hermes_host.get("observationMode") or cfg.get("observationMode", "directional")
|
||||
print("\n Observation mode:")
|
||||
|
|
|
|||
|
|
@ -238,4 +238,152 @@ class TestCloneHonchoForProfile:
|
|||
new_block = written["cfg"]["hosts"]["hermes.coder"]
|
||||
assert "userPeerAliases" not in new_block
|
||||
assert "runtimePeerPrefix" not in new_block
|
||||
assert "pinPeerName" not in new_block
|
||||
assert "pinPeerName" not in new_block
|
||||
|
||||
|
||||
class TestSetupWizardDeploymentShape:
|
||||
"""The deployment-shape step writes pinPeerName / userPeerAliases /
|
||||
runtimePeerPrefix based on the operator's chosen shape.
|
||||
|
||||
Single-operator deployments collapse all platforms to peerName.
|
||||
Multi-user gateways leave the resolver to route per-runtime.
|
||||
Hybrid deployments alias the operator's own runtime IDs only.
|
||||
|
||||
These tests script the interactive _prompt calls and assert the
|
||||
resulting hermes_host block, so the wizard's deployment-shape
|
||||
semantics stay locked even as adjacent prompts are added.
|
||||
"""
|
||||
|
||||
def _run_setup(self, monkeypatch, tmp_path, *, answers, initial_cfg=None):
|
||||
import plugins.memory.honcho.cli as honcho_cli
|
||||
|
||||
cfg_path = tmp_path / "config.json"
|
||||
cfg_path.write_text("{}")
|
||||
cfg = initial_cfg if initial_cfg is not None else {"apiKey": "***"}
|
||||
|
||||
monkeypatch.setattr(honcho_cli, "_read_config", lambda: cfg)
|
||||
monkeypatch.setattr(honcho_cli, "_config_path", lambda: cfg_path)
|
||||
monkeypatch.setattr(honcho_cli, "_local_config_path", lambda: cfg_path)
|
||||
monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes")
|
||||
monkeypatch.setattr(honcho_cli, "_ensure_sdk_installed", lambda: True)
|
||||
monkeypatch.setattr(honcho_cli, "_write_config", lambda *a, **k: None)
|
||||
|
||||
# Bypass config.yaml + connection test side effects.
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.load_config", lambda: {"memory": {}}, raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.save_config", lambda c: None, raising=False,
|
||||
)
|
||||
|
||||
class _FakeClientCfg:
|
||||
def resolve_session_name(self):
|
||||
return "hermes-test"
|
||||
workspace_id = "hermes"
|
||||
peer_name = "eri"
|
||||
ai_peer = "hermetika"
|
||||
observation_mode = "directional"
|
||||
write_frequency = "async"
|
||||
recall_mode = "hybrid"
|
||||
session_strategy = "per-session"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
|
||||
lambda host=None: _FakeClientCfg(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.memory.honcho.client.reset_honcho_client",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.memory.honcho.client.get_honcho_client",
|
||||
lambda hcfg: object(),
|
||||
)
|
||||
|
||||
# Scripted _prompt: pop answers in order. Default-return for unconsumed prompts.
|
||||
answer_iter = iter(answers)
|
||||
def _scripted_prompt(label, default=None, secret=False):
|
||||
try:
|
||||
return next(answer_iter)
|
||||
except StopIteration:
|
||||
return default if default is not None else ""
|
||||
monkeypatch.setattr(honcho_cli, "_prompt", _scripted_prompt)
|
||||
|
||||
honcho_cli.cmd_setup(SimpleNamespace())
|
||||
return cfg["hosts"]["hermes"]
|
||||
|
||||
def test_single_shape_sets_pin_peer_name_and_clears_aliases(self, monkeypatch, tmp_path):
|
||||
answers = [
|
||||
"cloud", # deployment
|
||||
"", # api key (keep)
|
||||
"eri", # peer name
|
||||
"hermetika", # ai peer
|
||||
"hermes", # workspace
|
||||
"single", # deployment shape ← key answer
|
||||
# remaining prompts fall through to defaults
|
||||
]
|
||||
initial_cfg = {
|
||||
"apiKey": "***",
|
||||
"hosts": {"hermes": {
|
||||
"userPeerAliases": {"old": "stale"},
|
||||
"runtimePeerPrefix": "old_",
|
||||
}},
|
||||
}
|
||||
host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
|
||||
assert host["pinPeerName"] is True
|
||||
assert "userPeerAliases" not in host
|
||||
assert "runtimePeerPrefix" not in host
|
||||
|
||||
def test_multi_shape_leaves_pin_false_and_accepts_prefix(self, monkeypatch, tmp_path):
|
||||
answers = [
|
||||
"cloud", # deployment
|
||||
"", # api key (keep)
|
||||
"eri", # peer name
|
||||
"hermetika", # ai peer
|
||||
"hermes", # workspace
|
||||
"multi", # deployment shape
|
||||
"telegram_", # runtime peer prefix
|
||||
]
|
||||
host = self._run_setup(monkeypatch, tmp_path, answers=answers)
|
||||
assert host["pinPeerName"] is False
|
||||
assert host["userPeerAliases"] == {}
|
||||
assert host["runtimePeerPrefix"] == "telegram_"
|
||||
|
||||
def test_hybrid_shape_aliases_operator_runtime_ids_to_peer_name(self, monkeypatch, tmp_path):
|
||||
answers = [
|
||||
"cloud", # deployment
|
||||
"", # api key (keep)
|
||||
"eri", # peer name
|
||||
"hermetika", # ai peer
|
||||
"hermes", # workspace
|
||||
"hybrid", # deployment shape
|
||||
"86701400", # telegram uid
|
||||
"491827364", # discord snowflake
|
||||
"", # slack (skip)
|
||||
"", # matrix (skip)
|
||||
"", # runtime peer prefix (skip)
|
||||
]
|
||||
host = self._run_setup(monkeypatch, tmp_path, answers=answers)
|
||||
assert host["pinPeerName"] is False
|
||||
assert host["userPeerAliases"] == {
|
||||
"86701400": "eri",
|
||||
"491827364": "eri",
|
||||
}
|
||||
assert "runtimePeerPrefix" not in host
|
||||
|
||||
def test_skip_shape_preserves_existing_identity_config(self, monkeypatch, tmp_path):
|
||||
initial_cfg = {
|
||||
"apiKey": "***",
|
||||
"hosts": {"hermes": {
|
||||
"pinPeerName": True,
|
||||
"userPeerAliases": {"keep": "me"},
|
||||
"runtimePeerPrefix": "keep_",
|
||||
}},
|
||||
}
|
||||
answers = [
|
||||
"cloud", "", "eri", "hermetika", "hermes", "skip",
|
||||
]
|
||||
host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
|
||||
assert host["pinPeerName"] is True
|
||||
assert host["userPeerAliases"] == {"keep": "me"}
|
||||
assert host["runtimePeerPrefix"] == "keep_"
|
||||
Loading…
Add table
Add a link
Reference in a new issue