From 92179352fb7621a04a3592082ac30873759d0cff Mon Sep 17 00:00:00 2001 From: underthestars-zhy Date: Mon, 8 Jun 2026 17:26:48 -0700 Subject: [PATCH] feat(photon): auto-configure allowlist and cron channel on setup During `hermes photon setup`, allowlist the operator's number and set their DM as the cron home channel when those env vars are unset. Without this, the gateway denies the operator's own messages and cron has no default delivery target. Re-runs never overwrite hand-tuned values. Also teaches the sidecar's `resolveSpace` to accept a bare E.164 number as a space identifier, resolving it to the user's DM space so `PHOTON_HOME_CHANNEL` can be set to a phone number instead of an opaque space id. --- plugins/platforms/photon/README.md | 4 +- plugins/platforms/photon/cli.py | 31 +++++++++++++++ plugins/platforms/photon/sidecar/index.mjs | 20 ++++++++++ .../platforms/photon/test_setup_access.py | 38 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/plugins/platforms/photon/test_setup_access.py diff --git a/plugins/platforms/photon/README.md b/plugins/platforms/photon/README.md index 7865649cdb6..f07acf4f25c 100644 --- a/plugins/platforms/photon/README.md +++ b/plugins/platforms/photon/README.md @@ -111,8 +111,8 @@ All env vars are documented in `plugin.yaml`. The most important: | `PHOTON_SIDECAR_PORT` | 8789 | Loopback port for the sidecar | | `PHOTON_SIDECAR_AUTOSTART`| true | Spawn the sidecar on connect | | `PHOTON_DASHBOARD_HOST` | https://app.photon.codes | Dashboard API host | -| `PHOTON_HOME_CHANNEL` | (unset) | Default space id for cron delivery | -| `PHOTON_ALLOWED_USERS` | (unset) | Comma-separated E.164 allowlist | +| `PHOTON_HOME_CHANNEL` | your number (set by setup) | Default space for cron delivery — a space id, or a bare E.164 number (resolved to a DM) | +| `PHOTON_ALLOWED_USERS` | your number (set by setup) | Comma-separated E.164 allowlist | | `PHOTON_REQUIRE_MENTION` | false | Gate group chats on a wake word | | `PHOTON_MAX_INLINE_ATTACHMENT_BYTES` | 20 MB | Max inbound attachment size the sidecar reads & inlines | diff --git a/plugins/platforms/photon/cli.py b/plugins/platforms/photon/cli.py index 1ddf6014d30..730b72d38e2 100644 --- a/plugins/platforms/photon/cli.py +++ b/plugins/platforms/photon/cli.py @@ -210,6 +210,10 @@ def _cmd_setup(args: argparse.Namespace) -> int: # no dedicated entry in /lines, so this per-user field is the source of # truth — and we already have it from the (reused) user object. agent_number = photon_auth.user_assigned_line(user) + # Allowlist the operator and make their DM the cron home channel — + # otherwise the gateway denies their own inbound messages + # ("Unauthorized user") and has no default space for cron delivery. + _autoconfigure_access(phone) # 5. Surface the agent's iMessage number (the number to text the agent). if not agent_number: @@ -248,6 +252,33 @@ def _cmd_setup(args: argparse.Namespace) -> int: return 0 +def _autoconfigure_access(phone: str) -> None: + """Allowlist the operator and set their DM as the cron home channel. + + Writes ``PHOTON_ALLOWED_USERS`` (so the gateway authorizes the operator's + own inbound messages instead of denying them) and ``PHOTON_HOME_CHANNEL`` + (the default space for cron delivery) to the operator's E.164 number. Each + is only filled when unset, so a hand-tuned allowlist / home channel is + never clobbered on a re-run. + """ + try: + from hermes_cli.config import get_env_value, save_env_value + except ImportError: + return + for key, label in ( + ("PHOTON_ALLOWED_USERS", "allowlisted your number"), + ("PHOTON_HOME_CHANNEL", "set your DM as the cron home channel"), + ): + try: + if get_env_value(key): + print(f" {key} already set — leaving it as-is.") + continue + save_env_value(key, phone) + print(f" ✓ {label} ({key})") + except Exception as e: + print(f" could not set {key}: {e}", file=sys.stderr) + + def _cmd_status(_args: argparse.Namespace) -> int: # Defer the credential rows to auth.print_credential_summary — its emit # callback is the only sink that sees credential-derived strings, so diff --git a/plugins/platforms/photon/sidecar/index.mjs b/plugins/platforms/photon/sidecar/index.mjs index 60f5ba2a8c8..5371a683d9a 100644 --- a/plugins/platforms/photon/sidecar/index.mjs +++ b/plugins/platforms/photon/sidecar/index.mjs @@ -303,6 +303,26 @@ function handleInbound(req, res) { } async function resolveSpace(spaceId) { + // A bare E.164 phone number addresses a DM. Resolve the user, then the (DM) + // space — `imessage(app).user(phone)` -> `im.space(user)` — so callers can + // pass just "+1..." (e.g. PHOTON_HOME_CHANNEL for cron delivery) instead of + // an opaque inbound space id. Real inbound space ids never match this shape, + // so this only kicks in for phone-addressed sends. + if (typeof spaceId === "string" && /^\+\d{6,}$/.test(spaceId) && imessage) { + try { + const im = imessage(app); + if (typeof im.user === "function" && typeof im.space === "function") { + const user = await im.user(spaceId); + return await im.space(user); + } + } catch (e) { + console.error( + "photon-sidecar: phone->DM resolution failed; falling back to " + + "id-based lookup: " + + (e && e.stack ? e.stack : String(e)) + ); + } + } // spectrum-ts exposes the same Space methods via `app.space(spaceId)` / // narrowed helpers; we fall back through a few accessor shapes to // tolerate small SDK API drift. diff --git a/tests/plugins/platforms/photon/test_setup_access.py b/tests/plugins/platforms/photon/test_setup_access.py new file mode 100644 index 00000000000..8a9d7cb864a --- /dev/null +++ b/tests/plugins/platforms/photon/test_setup_access.py @@ -0,0 +1,38 @@ +"""Tests for `hermes photon setup`'s access auto-configuration. + +`_autoconfigure_access` allowlists the operator and points the cron home +channel at their DM, writing to the per-test ~/.hermes/.env (the hermetic +HERMES_HOME fixture isolates this). It must fill only unset keys so a re-run +never clobbers a hand-tuned allowlist. +""" +from __future__ import annotations + +import pytest + +from hermes_cli.config import get_env_value, save_env_value +from plugins.platforms.photon import cli + + +def test_autoconfigure_access_fills_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PHOTON_ALLOWED_USERS", raising=False) + monkeypatch.delenv("PHOTON_HOME_CHANNEL", raising=False) + + cli._autoconfigure_access("+15551234567") + + assert get_env_value("PHOTON_ALLOWED_USERS") == "+15551234567" + assert get_env_value("PHOTON_HOME_CHANNEL") == "+15551234567" + + +def test_autoconfigure_access_preserves_existing_allowlist( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PHOTON_ALLOWED_USERS", raising=False) + monkeypatch.delenv("PHOTON_HOME_CHANNEL", raising=False) + # A hand-tuned allowlist already in place must survive a setup re-run. + save_env_value("PHOTON_ALLOWED_USERS", "+19998887777,+15551112222") + + cli._autoconfigure_access("+15551234567") + + assert get_env_value("PHOTON_ALLOWED_USERS") == "+19998887777,+15551112222" + # The still-unset home channel is filled. + assert get_env_value("PHOTON_HOME_CHANNEL") == "+15551234567"