feat(photon): gRPC-native iMessage channel (no webhook) - #42582
Merged
Conversation
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.
Contributor
🔎 Lint report:
|
| 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.
1 task
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Photon iMessage becomes a fully persistent-connection channel — both inbound and outbound run over the
spectrum-tsgRPC 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
mainwith authorship preserved.Changes
plugins/platforms/photon/: removed the aiohttp webhook server, HMAC verification, public-URL requirement, and allPHOTON_WEBHOOK_*config; inbound now consumes the SDKapp.messagesgRPC stream through the sidecar (NDJSON over loopback, dedupe + reconnect backoff).space.send(attachment(...)).hermes photon setup(login + webhook subcommands removed);client_id=photon-climatches the official allowlisted CLI.tools/send_message_tool.py: one-line add of"photon"to_PHONE_PLATFORMSso a bare E.164 target resolves to a Photon DM (shared chokepoint, not per-platform special-casing).scripts/release.py: AUTHOR_MAP entry for the contributor (changelog attribution).Validation
login+webhook+ setuphermes photon setupCloses #42444.
Infographic