Skip to content

feat(photon): gRPC-native iMessage channel (no webhook) - #42582

Merged
teknium1 merged 12 commits into
mainfrom
hermes/hermes-7c9803a5
Jun 9, 2026
Merged

feat(photon): gRPC-native iMessage channel (no webhook)#42582
teknium1 merged 12 commits into
mainfrom
hermes/hermes-7c9803a5

Conversation

@teknium1

@teknium1 teknium1 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Photon iMessage becomes a fully persistent-connection channel — both inbound and outbound run over the spectrum-ts gRPC stream via the Node sidecar, eliminating the public-URL/webhook requirement that made it the odd channel out.

Salvage of #42444 by @underthestars-zhy, cherry-picked onto current main with authorship preserved.

Changes

  • plugins/platforms/photon/: removed the aiohttp webhook server, HMAC verification, public-URL requirement, and all PHOTON_WEBHOOK_* config; inbound now consumes the SDK app.messages gRPC stream through the sidecar (NDJSON over loopback, dedupe + reconnect backoff).
  • Bidirectional attachments: inbound downloaded/base64-inlined/cached (BlueBubbles parity, vision included, 20 MB cap → text marker fallback); outbound images/voice/video/docs via space.send(attachment(...)).
  • Onboarding collapses to a single hermes photon setup (login + webhook subcommands removed); client_id=photon-cli matches the official allowlisted CLI.
  • tools/send_message_tool.py: one-line add of "photon" to _PHONE_PLATFORMS so a bare E.164 target resolves to a Photon DM (shared chokepoint, not per-platform special-casing).
  • Docs: plugin README + website messaging docs rewritten for the gRPC architecture.
  • scripts/release.py: AUTHOR_MAP entry for the contributor (changelog attribution).

Validation

Before (main) After
Inbound transport signed aiohttp webhook (public URL) gRPC stream via sidecar (loopback)
Outbound attachments none images/voice/video/docs
Onboarding login + webhook + setup single hermes photon setup
Photon + send_message tests 195 passed

Closes #42444.

Infographic

photon-grpc-native-imessage

underthestars-zhy and others added 12 commits June 8, 2026 20:40
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>
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`.
Salvage follow-up for PR #42444 — maps the contributor's commit email
so the changelog generator can attribute the Photon gRPC channel work.
@teknium1
teknium1 requested a review from a team June 9, 2026 03:43
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-7c9803a5 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10561 on HEAD, 10579 on base (✅ -18)

🆕 New issues (2):

Rule Count
invalid-assignment 1
unresolved-import 1
First entries
plugins/platforms/photon/adapter.py:147: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["home_channel"]` and value of type `dict[str, str]` on object of type `dict[str, str]`
tests/plugins/platforms/photon/test_setup_access.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues (9):

Rule Count
unresolved-attribute 7
unresolved-import 2
First entries
plugins/platforms/photon/adapter.py:477: [unresolved-attribute] unresolved-attribute: Attribute `Response` is not defined on `None` in union `Unknown | None`
plugins/platforms/photon/adapter.py:422: [unresolved-attribute] unresolved-attribute: Attribute `AppRunner` is not defined on `None` in union `Unknown | None`
plugins/platforms/photon/adapter.py:52: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
plugins/platforms/photon/adapter.py:435: [unresolved-attribute] unresolved-attribute: Attribute `Request` is not defined on `None` in union `Unknown | None`
plugins/platforms/photon/adapter.py:419: [unresolved-attribute] unresolved-attribute: Attribute `Application` is not defined on `None` in union `Unknown | None`
plugins/platforms/photon/adapter.py:424: [unresolved-attribute] unresolved-attribute: Attribute `TCPSite` is not defined on `None` in union `Unknown | None`
plugins/platforms/photon/adapter.py:607: [unresolved-attribute] unresolved-attribute: Attribute `RequestError` is not defined on `None` in union `Unknown | None`
tests/plugins/platforms/photon/test_signature.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/photon/adapter.py:924: [unresolved-attribute] unresolved-attribute: Attribute `AsyncClient` is not defined on `None` in union `Unknown | None`

Unchanged: 5548 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have tool/web Web search and extraction type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants