Skip to content

feat(photon): gRPC-native iMessage channel (persistent connection, no webhook) - #42444

Closed
underthestars-zhy wants to merge 14 commits into
NousResearch:mainfrom
photon-hq:ryan/photon-fix
Closed

feat(photon): gRPC-native iMessage channel (persistent connection, no webhook)#42444
underthestars-zhy wants to merge 14 commits into
NousResearch:mainfrom
photon-hq:ryan/photon-fix

Conversation

@underthestars-zhy

@underthestars-zhy underthestars-zhy commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Reworks the Photon iMessage platform plugin into a first-class persistent-connection channel — like Discord and Slack — backed by the spectrum-ts gRPC stream for both inbound and outbound.

Previously Photon relied on inbound webhooks (a public URL, an aiohttp server, and HMAC signature verification) plus a separate management/login flow. That model is fragile (requires a reachable public URL), doesn't match how every other Hermes channel onboards, and couldn't deliver real attachments.

The new design holds a long-lived gRPC stream to Photon inside a small supervised Node sidecar (because spectrum-ts is TypeScript-only) and talks to the Python adapter over loopback. The whole webhook stack is gone, onboarding collapses to a single hermes photon setup, and attachments now work in both directions.

                         gRPC (spectrum-ts)
┌─────────────────────────┐ ◄───────────────► ┌──────────────────────┐
│  Photon Spectrum cloud  │   app.messages    │  Node sidecar        │
│  (iMessage line owner)  │   space.send()    │  (plugins/…/sidecar) │
└─────────────────────────┘                   └──────────┬───────────┘
                                  GET /inbound (NDJSON) │  ▲ POST /send
                                  inbound events        ▼  │ /typing
                                              ┌──────────────────────┐
                                              │  PhotonAdapter        │
                                              │  (Python, in gateway) │
                                              └──────────────────────┘

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Inbound: gRPC stream instead of webhooks

  • The sidecar consumes the SDK's app.messages gRPC stream, normalizes each message, and streams it to the adapter over a loopback GET /inbound (NDJSON). — plugins/platforms/photon/sidecar/index.mjs
  • PhotonAdapter consumes the NDJSON stream, dedupes on messageId, dispatches MessageEvent, and reconnects with backoff if the stream drops (the sidecar owns the gRPC reconnect to Photon). — plugins/platforms/photon/adapter.py
  • Removed the aiohttp webhook server, HMAC signature verification, public-URL requirement, and all PHOTON_WEBHOOK_* config. — adapter.py, plugin.yaml

Outbound: spectrum-ts send path

  • send / send_typing are loopback POSTs to the sidecar (/send, /typing), authenticated with a shared X-Hermes-Sidecar-Token. — adapter.py, sidecar/index.mjs
  • Text sends are wrapped with the spectrumText helper; threaded replies use the spectrum-ts reply builder. — sidecar/index.mjs
  • E.164 numbers and DM GUIDs are accepted as send targets; the home channel resolves a bare number to a DM space. — adapter.py, tools/send_message_tool.py

Attachments (both directions)

  • Inbound attachments are downloaded by the sidecar (content.read()), base64-inlined on the NDJSON event, cached to the shared media cache, and exposed via media_urls / media_types (vision included) — parity with BlueBubbles. Oversized (default 20 MB) or failed reads fall back to a text marker. — sidecar/index.mjs, adapter.py
  • Outbound images, voice notes, video, and documents are sent via space.send(attachment(...)) / space.send(voice(...)) through the sidecar's attachment endpoint. — sidecar/index.mjs

Management plane & setup

  • Device login (RFC 8628) uses client_id=photon-cli against the single dashboard host, matching the official photon-hq/cli. — auth.py
  • Setup finds-or-creates the Hermes Agent project, enables Spectrum, reads the project's spectrumProjectId (distinct from the dashboard id — the SDK authenticates with the spectrum id), rotates the project secret, and registers the user with phone dedup. — auth.py, cli.py
  • Runtime SDK creds (PHOTON_PROJECT_ID, PHOTON_PROJECT_SECRET) persist to ~/.hermes/.env like every other channel; management metadata lives in ~/.hermes/auth.json. — auth.py
  • hermes photon setup is now a single 6-step flow; the separate login and webhook subcommands are removed. Setup also auto-configures the allowlist and home/cron channel and prints the assigned iMessage line. — cli.py

Docs & deps

  • Rewrote the plugin README and the website messaging docs for the gRPC architecture. — plugins/platforms/photon/README.md, website/docs/user-guide/messaging/photon.md
  • Sidecar pins spectrum-ts ^1.17.1. — sidecar/package.json, sidecar/package-lock.json

How to Test

# 1. One-shot setup: device login (opens browser) + project + user + sidecar deps
hermes photon setup --phone +15551234567

# 2. Start the gateway
hermes gateway start --platform photon
  1. Text the assigned iMessage line printed by setup → the agent replies over the gRPC stream (no public URL / webhook needed).
  2. Send an image to the agent → it's downloaded, cached, and the agent sees it (vision).
  3. Ask the agent to send an image/voice note back → delivered via space.send(attachment(...)).
  4. Kill network briefly → the adapter reconnects to the sidecar and the sidecar re-establishes the gRPC stream.

Automated:

pytest tests/plugins/platforms/photon/ tests/tools/test_send_message_target_parse.py tests/tools/test_send_message_tool.py -q
# 62 passed, 1 skipped

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass (Photon scope: 62 passed, 1 skipped)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS (Darwin)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — plugin README + website messaging docs
  • I've updated cli-config.yaml.example if I added/changed config keys — env vars documented in plugin.yaml
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — sidecar runs on Node; iMessage targets macOS
  • I've updated tool descriptions/schemas if I changed tool behavior — send_message_tool target parsing

Screenshots / Logs

$ pytest tests/plugins/platforms/photon/ tests/tools/test_send_message_target_parse.py tests/tools/test_send_message_tool.py -q
..............................................................           [100%]
62 passed, 1 skipped in 12.00s

underthestars-zhy and others added 2 commits June 8, 2026 16:09
Make Photon iMessage a first-class persistent-connection channel like
Discord/Slack, using the spectrum-ts gRPC stream for both directions.

- Inbound: the sidecar forwards the SDK's app.messages gRPC stream to the
  adapter over a loopback GET /inbound (NDJSON) instead of webhooks. Drops
  the aiohttp webhook server, HMAC signature verification, public URL, and
  PHOTON_WEBHOOK_* config; adapter reconnects with backoff.
- Management plane: device login uses client_id=photon-cli against the
  single dashboard host (Bearer), matching the official photon-hq/cli;
  find-or-create "Hermes Agent" project, enable Spectrum, rotate secret,
  register user (with phone dedup), surface the assigned iMessage line.
- SDK projectId is the project's spectrumProjectId, not the dashboard id;
  runtime creds persist to ~/.hermes/.env like every other channel.
- CLI: 6-step setup, webhook subcommands removed.
- Tests/docs updated for the gRPC flow; sidecar pins spectrum-ts ^1.17.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery labels Jun 8, 2026
On shared-number plans, `/lines` has no dedicated entry, so the
`assignedPhoneNumber` field on the user object is the source of truth
for which number to text the agent. Fall back to the line inventory
only when no per-user assignment exists.
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.
Allow PHOTON_HOME_CHANNEL to accept a bare E.164 phone number or a
`any;-;+1...` DM chat GUID in addition to a Spectrum space id. Inbound
DM spaces are cached so replies resolve without a second SDK lookup,
and `photon` is added to _PHONE_PLATFORMS so send_message treats E.164
strings as explicit targets rather than falling through to channel-name
resolution.
Replace raw `{ replyTo }` send options with the `spectrumReply` content
builder from spectrum-ts, which is the correct API for threading
replies.
Adds `maybeReplyContent` helper with graceful fallback to normal send
when
the reply target cannot be resolved.
Drop `replyTo` from all outbound send paths and update the `/typing`
endpoint to use the documented `typing("start" | "stop")` content
builder. Adds a `stop_typing` method on the adapter to pair with
`send_typing`.
@underthestars-zhy
underthestars-zhy marked this pull request as ready for review June 9, 2026 02:59
@underthestars-zhy
underthestars-zhy requested a review from a team June 9, 2026 02:59
@underthestars-zhy underthestars-zhy changed the title Ryan/photon fix feat(photon): gRPC-native iMessage channel (persistent connection, no webhook) Jun 9, 2026
Store operator and assigned iMessage numbers in `auth.json` after
setup, and surface them in `hermes photon status`. When numbers are
missing, status auto-refreshes from the dashboard without provisioning
new lines.
teknium1 added a commit that referenced this pull request Jun 9, 2026
Salvage follow-up for PR #42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
Salvage follow-up for PR #42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
davidgut1982 pushed a commit to davidgut1982/hermes-agent that referenced this pull request Jun 17, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
donbowman pushed a commit to donbowman/hermes-agent that referenced this pull request Jul 13, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
Salvage follow-up for PR NousResearch#42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants