mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(photon): skip secret regeneration when existing credentials are valid
hermes photon setup unconditionally called regenerate_project_secret() on every re-run, invalidating the credential held by a running sidecar and causing all outbound sends to fail with AuthenticationError. Now validates existing credentials via a lightweight list_users call before deciding to regenerate. Only rotates when no credentials exist or the existing ones are invalid, and warns the user to restart the gateway when rotation occurs. Fixes #50755
This commit is contained in:
parent
eccf39dded
commit
703de9bacb
2 changed files with 139 additions and 6 deletions
|
|
@ -177,15 +177,32 @@ def _cmd_setup(args: argparse.Namespace) -> int:
|
|||
print("could not resolve a Photon project id", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 3. Rotate the project secret and persist creds (runtime -> ~/.hermes/.env,
|
||||
# 3. Provision Spectrum credentials (runtime -> ~/.hermes/.env,
|
||||
# ids -> auth.json). Spectrum is always enabled and provisioned at
|
||||
# create-time, and the dashboard project id *is* the Spectrum project id
|
||||
# (ids unified), so there's nothing to enable — the id we already have is
|
||||
# the Spectrum id.
|
||||
#
|
||||
# On re-run we reuse an existing valid secret instead of regenerating.
|
||||
# Regenerating invalidates the credential that a running sidecar holds
|
||||
# in its process env, causing all outbound sends to fail with
|
||||
# AuthenticationError until the gateway is restarted (GH #50755).
|
||||
try:
|
||||
print("[3/5] Provisioning Spectrum credentials...")
|
||||
spectrum_id = dashboard_id
|
||||
secret = photon_auth.regenerate_project_secret(token, dashboard_id)
|
||||
existing_id, existing_secret = photon_auth.load_project_credentials()
|
||||
secret: str = ""
|
||||
reused = False
|
||||
if existing_id and existing_secret:
|
||||
# Validate the existing credential with a lightweight API call.
|
||||
try:
|
||||
photon_auth.list_users(existing_id, existing_secret)
|
||||
secret = existing_secret
|
||||
reused = True
|
||||
except Exception:
|
||||
secret = "" # fall through to regeneration
|
||||
if not secret:
|
||||
secret = photon_auth.regenerate_project_secret(token, dashboard_id)
|
||||
photon_auth.store_project_credentials(
|
||||
spectrum_project_id=spectrum_id,
|
||||
project_secret=secret,
|
||||
|
|
@ -193,7 +210,15 @@ def _cmd_setup(args: argparse.Namespace) -> int:
|
|||
name=name,
|
||||
)
|
||||
# spectrum_id is an opaque non-secret id; safe to show.
|
||||
print(f" ✓ Spectrum ready (project id {spectrum_id}) — secret saved")
|
||||
if reused:
|
||||
print(f" ✓ Spectrum ready (project id {spectrum_id}) — existing credentials valid")
|
||||
else:
|
||||
print(f" ✓ Spectrum ready (project id {spectrum_id}) — new secret saved")
|
||||
print(
|
||||
" ⚠ Project secret was regenerated. If the gateway is running, "
|
||||
"restart it so the sidecar picks up the new secret:\n"
|
||||
" hermes gateway restart"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"spectrum provisioning failed: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ def test_setup_hint_uses_gateway_service_command(monkeypatch: pytest.MonkeyPatch
|
|||
# longer enables Spectrum or fetches a separate spectrumProjectId — it
|
||||
# reuses this id directly.
|
||||
monkeypatch.setattr(cli.photon_auth, "load_dashboard_project_id", lambda: "dashboard")
|
||||
# No existing credentials — first-time setup path.
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth, "load_project_credentials", lambda: (None, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"regenerate_project_secret",
|
||||
|
|
@ -86,16 +90,16 @@ def test_setup_hint_uses_gateway_service_command(monkeypatch: pytest.MonkeyPatch
|
|||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"register_user_if_absent",
|
||||
lambda *args, **kwargs: ({"id": "user_123", "phoneNumber": "+15551234567"}, True),
|
||||
lambda *args, **kwargs: ({"id": "user_123", "phoneNumber": "+155****4567"}, True),
|
||||
)
|
||||
monkeypatch.setattr(cli.photon_auth, "user_assigned_line", lambda user: "+15557654321")
|
||||
monkeypatch.setattr(cli.photon_auth, "user_assigned_line", lambda user: "+155****4321")
|
||||
monkeypatch.setattr(cli.photon_auth, "store_user_numbers", lambda **kwargs: None)
|
||||
monkeypatch.setattr(cli, "_install_sidecar", lambda: 0)
|
||||
|
||||
rc = cli._cmd_setup(
|
||||
argparse.Namespace(
|
||||
project_name=None,
|
||||
phone="+15551234567",
|
||||
phone="+155****4567",
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
email=None,
|
||||
|
|
@ -108,3 +112,107 @@ def test_setup_hint_uses_gateway_service_command(monkeypatch: pytest.MonkeyPatch
|
|||
out = capsys.readouterr().out
|
||||
assert "Start the gateway: hermes gateway start" in out
|
||||
assert "--platform photon" not in out
|
||||
assert "new secret saved" in out
|
||||
assert "restart it so the sidecar" in out
|
||||
|
||||
|
||||
def test_setup_reuses_valid_existing_secret(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys,
|
||||
) -> None:
|
||||
"""Re-running setup with a valid existing secret must NOT regenerate it."""
|
||||
regenerate_called = False
|
||||
|
||||
def _fake_regenerate(token, dashboard_id):
|
||||
nonlocal regenerate_called
|
||||
regenerate_called = True
|
||||
return "new_secret"
|
||||
|
||||
monkeypatch.setattr(cli.photon_auth, "load_photon_token", lambda: "token")
|
||||
monkeypatch.setattr(cli.photon_auth, "load_dashboard_project_id", lambda: "dashboard")
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"load_project_credentials",
|
||||
lambda: ("dashboard", "existing_secret"),
|
||||
)
|
||||
# list_users succeeds — existing secret is valid.
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth, "list_users", lambda pid, secret: [],
|
||||
)
|
||||
monkeypatch.setattr(cli.photon_auth, "regenerate_project_secret", _fake_regenerate)
|
||||
monkeypatch.setattr(cli.photon_auth, "store_project_credentials", lambda **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"register_user_if_absent",
|
||||
lambda *args, **kwargs: ({"id": "user_123", "phoneNumber": "+155****4567"}, True),
|
||||
)
|
||||
monkeypatch.setattr(cli.photon_auth, "user_assigned_line", lambda user: "+155****4321")
|
||||
monkeypatch.setattr(cli.photon_auth, "store_user_numbers", lambda **kwargs: None)
|
||||
monkeypatch.setattr(cli, "_install_sidecar", lambda: 0)
|
||||
|
||||
rc = cli._cmd_setup(
|
||||
argparse.Namespace(
|
||||
project_name=None,
|
||||
phone="+155****4567",
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
email=None,
|
||||
no_browser=True,
|
||||
skip_sidecar_install=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert rc == 0
|
||||
assert not regenerate_called, "regenerate_project_secret must not be called when existing creds are valid"
|
||||
out = capsys.readouterr().out
|
||||
assert "existing credentials valid" in out
|
||||
assert "restart" not in out.lower()
|
||||
|
||||
|
||||
def test_setup_regenerates_when_existing_secret_invalid(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys,
|
||||
) -> None:
|
||||
"""When existing credentials are invalid, setup must regenerate."""
|
||||
monkeypatch.setattr(cli.photon_auth, "load_photon_token", lambda: "token")
|
||||
monkeypatch.setattr(cli.photon_auth, "load_dashboard_project_id", lambda: "dashboard")
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"load_project_credentials",
|
||||
lambda: ("dashboard", "stale_secret"),
|
||||
)
|
||||
# list_users fails — existing secret is invalid.
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"list_users",
|
||||
lambda pid, secret: (_ for _ in ()).throw(RuntimeError("auth failed")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"regenerate_project_secret",
|
||||
lambda token, dashboard_id: "new_secret",
|
||||
)
|
||||
monkeypatch.setattr(cli.photon_auth, "store_project_credentials", lambda **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
cli.photon_auth,
|
||||
"register_user_if_absent",
|
||||
lambda *args, **kwargs: ({"id": "user_123", "phoneNumber": "+155****4567"}, True),
|
||||
)
|
||||
monkeypatch.setattr(cli.photon_auth, "user_assigned_line", lambda user: "+155****4321")
|
||||
monkeypatch.setattr(cli.photon_auth, "store_user_numbers", lambda **kwargs: None)
|
||||
monkeypatch.setattr(cli, "_install_sidecar", lambda: 0)
|
||||
|
||||
rc = cli._cmd_setup(
|
||||
argparse.Namespace(
|
||||
project_name=None,
|
||||
phone="+155****4567",
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
email=None,
|
||||
no_browser=True,
|
||||
skip_sidecar_install=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "new secret saved" in out
|
||||
assert "restart it so the sidecar" in out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue