From 630318e958bc28d4f45e86e67e771095bda0033b Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:33:47 -0700 Subject: [PATCH] refactor(photon): fold device login into setup, drop standalone login verb Every other Hermes gateway channel onboards through a single setup surface (paste a token / run the wizard) with no per-platform login command. Photon's device-code flow is unavoidable because Photon mints credentials via API rather than a copy-paste dashboard field, but exposing it as a top-level `hermes photon login` verb broke channel parity. - Remove the `login` subcommand; setup already runs the device flow as its first step. `--no-browser` moves onto `setup`. - Rename `_cmd_login` -> `_run_device_login` (internal helper). - Status / credential-summary hints now point at `hermes photon setup`. - README updated to the one-command onboarding flow. --- plugins/platforms/photon/README.md | 18 +++++++++++------- plugins/platforms/photon/auth.py | 4 ++-- plugins/platforms/photon/cli.py | 28 ++++++++++++++++------------ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/plugins/platforms/photon/README.md b/plugins/platforms/photon/README.md index b5c50a69151..5af7f02b8b2 100644 --- a/plugins/platforms/photon/README.md +++ b/plugins/platforms/photon/README.md @@ -46,25 +46,29 @@ plugin stays the same. ## First-time setup ```bash -# 1. Log in via the device-code flow (opens browser) -hermes photon login - -# 2. Full setup: project, user, sidecar deps +# 1. One-shot setup: device login (opens browser) + project + user + sidecar deps hermes photon setup --phone +15551234567 -# 3. Expose your webhook URL to the public internet +# 2. Expose your webhook URL to the public internet # (cloudflared, ngrok, your gateway's public hostname, etc.) # Then register it with Photon: hermes photon webhook register https://your-host.example.com/photon/webhook -# 4. Save the signing secret it prints to ~/.hermes/.env +# 3. Save the signing secret it prints to ~/.hermes/.env # as PHOTON_WEBHOOK_SECRET=... # Photon only returns it ONCE. -# 5. Start the gateway +# 4. Start the gateway hermes gateway start --platform photon ``` +`hermes photon setup` runs the RFC 8628 device-code login as its first +step — it opens `https://app.photon.codes/` for approval, then +provisions the Spectrum project + iMessage line. There is no separate +`login` command; like every other Hermes channel, onboarding goes +through one setup surface. Re-running `setup` reuses an existing token +and project, so it's safe to run again to finish a partial setup. + ## Credentials Stored in `~/.hermes/auth.json` under `credential_pool`: diff --git a/plugins/platforms/photon/auth.py b/plugins/platforms/photon/auth.py index 3ca2da4c467..e40edd66b4c 100644 --- a/plugins/platforms/photon/auth.py +++ b/plugins/platforms/photon/auth.py @@ -448,7 +448,7 @@ def print_credential_summary(emit: Any = print) -> None: if load_photon_token(): labels["device_token"] = "✓ stored" else: - labels["device_token"] = "✗ missing (run `hermes photon login`)" + labels["device_token"] = "✗ missing (run `hermes photon setup`)" pid, sec = load_project_credentials() labels["project_id"] = pid if pid else "✗ missing" labels["project_key"] = "✓ stored" if sec else "✗ missing" @@ -477,7 +477,7 @@ def credential_summary() -> Dict[str, str]: function — read-and-bool-cast happens entirely inside the closure. """ def _present_token() -> str: - return "✓ stored" if load_photon_token() else "✗ missing (run `hermes photon login`)" + return "✓ stored" if load_photon_token() else "✗ missing (run `hermes photon setup`)" def _present_project_id() -> str: pid, _sec = load_project_credentials() diff --git a/plugins/platforms/photon/cli.py b/plugins/platforms/photon/cli.py index 420eb4474ab..1316ace252b 100644 --- a/plugins/platforms/photon/cli.py +++ b/plugins/platforms/photon/cli.py @@ -4,13 +4,16 @@ Subcommands: - login run the device-code OAuth flow - setup full first-time setup (login + project + user + sidecar) + setup full first-time setup (device login + project + user + sidecar) status show login + project + sidecar dep state install-sidecar npm install inside plugins/platforms/photon/sidecar/ webhook register register the local webhook URL with Photon webhook list list registered webhooks webhook delete delete a webhook by id + +The device-code login runs automatically as the first step of ``setup``; +there is no standalone ``login`` verb (matching how every other Hermes +gateway channel onboards through a single setup surface). """ from __future__ import annotations @@ -35,17 +38,14 @@ def register_cli(parser: argparse.ArgumentParser) -> None: """Wire up `hermes photon ...` subcommands.""" subs = parser.add_subparsers(dest="photon_command", required=False) - p_login = subs.add_parser("login", help="Authenticate with Photon (device flow)") - p_login.add_argument("--no-browser", action="store_true", - help="Don't try to open a browser; print the URL only") - - p_setup = subs.add_parser("setup", help="First-time setup (login + project + user + sidecar)") + p_setup = subs.add_parser("setup", help="First-time setup (device login + project + user + sidecar)") p_setup.add_argument("--project-name", default=None, help="Project name (default: 'Hermes Agent')") p_setup.add_argument("--phone", default=None, help="Your E.164 phone number (e.g. +15551234567)") p_setup.add_argument("--first-name", default=None) p_setup.add_argument("--last-name", default=None) p_setup.add_argument("--email", default=None) - p_setup.add_argument("--no-browser", action="store_true") + p_setup.add_argument("--no-browser", action="store_true", + help="Don't try to open a browser for device login; print the URL only") p_setup.add_argument("--skip-sidecar-install", action="store_true", help="Skip `npm install` inside the sidecar directory") @@ -71,8 +71,6 @@ def dispatch(args: argparse.Namespace) -> int: if sub is None: # No subcommand given — show status by default. return _cmd_status(args) - if sub == "login": - return _cmd_login(args) if sub == "setup": return _cmd_setup(args) if sub == "status": @@ -88,7 +86,13 @@ def dispatch(args: argparse.Namespace) -> int: # --------------------------------------------------------------------------- # Subcommand handlers -def _cmd_login(args: argparse.Namespace) -> int: +def _run_device_login(args: argparse.Namespace) -> int: + """Run the RFC 8628 device-code login flow and persist the token. + + Internal helper — invoked as the first step of ``setup``. There is + no standalone ``hermes photon login`` command; Photon onboards + through the single ``setup`` surface like every other channel. + """ def _print_code(code): target = code.verification_uri_complete or code.verification_uri print() @@ -119,7 +123,7 @@ def _cmd_setup(args: argparse.Namespace) -> int: token = photon_auth.load_photon_token() if not token: print("[1/4] No Photon token found — running device login...") - rc = _cmd_login(args) + rc = _run_device_login(args) if rc != 0: return rc token = photon_auth.load_photon_token()