Brings Photon in line with how every other Hermes gateway channel behaves, instead of being a one-off with its own surfaces. - gateway setup: register a `setup_fn` so Photon appears in `hermes gateway setup` (the unified wizard) and runs the same device-login + project + user + sidecar flow as `hermes photon setup`. Adds `cli.gateway_setup()` as the zero-arg entry point. - PII redaction: flip `pii_safe` False -> True. The comment already said iMessage E.164 numbers should be redacted; the value contradicted it. Now matches BlueBubbles (the other iMessage channel) which is in _PII_SAFE_PLATFORMS — phone numbers are stripped before reaching the LLM. - Pairing/authz: already worked via the registry's allowed_users_env / allow_all_env generic path in authz_mixin; documented it. The adapter forwards unauthorized DMs to the gateway (no intake gating), so the pairing handshake fires and `hermes pairing approve photon <CODE>` works. - Docs: fixed the `hermes photon status` output block to match the real labels (project key / webhook key, not project secret / webhook secret), added the missing PHOTON_API_HOST / PHOTON_DASHBOARD_HOST / PHOTON_HOME_CHANNEL_NAME env vars, and added gateway-setup + authorize-users sections mirroring the other channel docs. Validation: 26/26 photon tests, 6504/6504 gateway+plugins tests, registry E2E confirms setup_fn dispatch + pii_safe + authz envs all wired.
7.3 KiB
| sidebar_position |
|---|
| 18 |
Photon iMessage
Connect Hermes to iMessage through Photon, a managed service that handles the Apple line allocation and abuse-prevention layer so you don't have to run your own Mac relay.
The free tier uses Photon's shared iMessage line pool — different recipients may see different sending numbers, but each conversation stays stable. The paid Business tier gives every user the same dedicated number; the plugin supports both, and the free tier is the recommended starting point.
:::info Free to start Photon's shared-line pool is free. No subscription is required to send your first iMessage from Hermes — just a phone number we can bind to your account. :::
Architecture
Inbound messages arrive as signed webhooks: Photon POSTs JSON with
an X-Spectrum-Signature header to a URL you register, and Hermes'
aiohttp listener verifies the HMAC-SHA256 signature before dispatching
the event into the agent.
Outbound replies go through a small supervised Node sidecar that
runs the spectrum-ts SDK on loopback. Photon does not currently
expose a public HTTP send-message endpoint — that's a roadmap item on
their side — so until then the sidecar is the only way to call
Space.send(...). The Python plugin starts, supervises, and shuts
down the sidecar automatically. When Photon ships an HTTP send
endpoint we'll retire the sidecar in a follow-up release.
Prerequisites
- A Photon account — sign up at app.photon.codes
- Node.js 18.17 or newer on PATH (
node --version) - A phone number that can receive iMessage (used to bind your account)
- A publicly reachable URL for the webhook receiver — Cloudflare Tunnel, ngrok, or your own gateway hostname all work
First-time setup
Either run the unified gateway wizard and pick Photon iMessage:
hermes gateway setup
…or run the Photon setup directly (the wizard calls the same flow):
# Device-code login + project + user + sidecar deps, all in one
hermes photon setup --phone +15551234567
The setup:
- Opens
https://app.photon.codes/for device approval - Creates a Spectrum-enabled project under your account
- Calls the Spectrum
create-userendpoint withtype: sharedso Photon allocates an iMessage line from the free pool - Runs
npm installinside the plugin's sidecar directory
Credentials are stored in ~/.hermes/auth.json under
credential_pool.photon (bearer token) and
credential_pool.photon_project (project id + secret).
Authorizing users
Photon uses the same authorization model as every other Hermes channel. Choose one approach:
DM pairing (default). When an unknown number messages your Photon line, Hermes replies with a pairing code. Approve it with:
hermes pairing approve photon <CODE>
Use hermes pairing list to see pending codes and approved users.
Pre-authorize specific numbers (in ~/.hermes/.env):
PHOTON_ALLOWED_USERS=+15551234567,+15559876543
Open access (dev only, in ~/.hermes/.env):
PHOTON_ALLOW_ALL_USERS=true
When PHOTON_ALLOWED_USERS is set, unknown senders are silently
ignored rather than offered a pairing code (the allowlist signals you
deliberately restricted access).
Registering the webhook
Photon needs a public URL it can POST to. Expose your local listener
(default port 8788, path /photon/webhook) via Cloudflare Tunnel or
ngrok, then:
hermes photon webhook register https://YOUR-PUBLIC-URL/photon/webhook
The response includes a signingSecret — Photon only returns it
once. Save it to ~/.hermes/.env:
PHOTON_WEBHOOK_SECRET=v0_64-char-hex...
The plugin verifies every inbound POST against this secret and
rejects deliveries with a timestamp drift greater than 5 minutes.
Start the gateway
hermes gateway start --platform photon
You'll see something like:
[photon] connected — webhook at 0.0.0.0:8788/photon/webhook, sidecar on 127.0.0.1:8789
Send an iMessage to your assigned number and Hermes will reply.
Status & troubleshooting
hermes photon status
Prints:
Photon iMessage status
──────────────────────
device token : ✓ stored
project id : 3c90c3cc-0d44-4b50-...
project key : ✓ stored
webhook key : ✓ set
node binary : /usr/bin/node
sidecar deps : ✓ installed
Common issues:
sidecar deps : ✗ run hermes photon install-sidecar— Node is installed butspectrum-tsisn't. Run the suggested command.webhook key : ⚠ unset — verification disabled— the plugin will accept ANY POST to the webhook URL, which is unsafe. Re-runhermes photon webhook registerand store the secret.PHOTON_WEBHOOK_PORTalready in use — set a different port via~/.hermes/.env.- Webhook reachable from localhost but Photon can't deliver — Photon needs a public hostname. Cloudflare Tunnel is the easiest free option.
Webhook management
hermes photon webhook list # show registered hooks
hermes photon webhook delete <webhook-id> # remove one
Limits today
- Attachments are metadata-only. Inbound webhooks carry the filename + MIME type but no download URL — Photon documents an attachment retrieval endpoint as roadmap.
- Outbound attachments not wired yet. Easy to add in the sidecar once the agent has reason to send them.
- Photon's free quotas: 5,000 messages per server per day,
50 new-conversation initiations per shared line per day. Increases
available — email
help@photon.codes.
Env vars
| Variable | Default | Notes |
|---|---|---|
PHOTON_PROJECT_ID |
from auth.json |
Set by hermes photon setup |
PHOTON_PROJECT_SECRET |
from auth.json |
Set by hermes photon setup |
PHOTON_WEBHOOK_SECRET |
(unset) | From hermes photon webhook register |
PHOTON_WEBHOOK_PORT |
8788 |
Local port for the aiohttp listener |
PHOTON_WEBHOOK_PATH |
/photon/webhook |
Path under which the listener mounts |
PHOTON_WEBHOOK_BIND |
0.0.0.0 |
Bind address for the listener |
PHOTON_SIDECAR_PORT |
8789 |
Loopback port for sidecar control |
PHOTON_SIDECAR_AUTOSTART |
true |
Whether the adapter spawns the sidecar |
PHOTON_NODE_BIN |
which node |
Override the Node binary path |
PHOTON_HOME_CHANNEL |
(unset) | Default space ID for cron / notifications |
PHOTON_HOME_CHANNEL_NAME |
(unset) | Human label for the home channel |
PHOTON_ALLOWED_USERS |
(unset) | Comma-separated E.164 allowlist |
PHOTON_ALLOW_ALL_USERS |
false |
Dev only — accept any sender |
PHOTON_API_HOST |
spectrum.photon.codes |
Override the Spectrum management API host |
PHOTON_DASHBOARD_HOST |
app.photon.codes |
Override the dashboard / device-login host |