First-class iMessage support via Photon's managed Spectrum platform. Targeted as a successor to the BlueBubbles adapter — Photon allocates the iMessage line, handles delivery, and abuse-prevention so users don't have to run their own Mac relay. Free tier uses Photon's shared line pool. Architecture: - Inbound: signed JSON webhooks (X-Spectrum-Signature, HMAC-SHA256) delivered to a local aiohttp listener. Dedupes on message.id, rejects deliveries with >5min timestamp drift. - Outbound: small supervised Node sidecar that runs the spectrum-ts SDK. Photon does not currently expose a public HTTP send-message endpoint; the sidecar is the only way to call Space.send() today. When Photon ships an HTTP send endpoint we collapse the sidecar into _sidecar_send and drop the Node dep — every other layer of the plugin stays the same. - Setup: 'hermes photon login' runs the RFC 8628 device-code flow; 'hermes photon setup' creates a Spectrum-enabled project, creates a shared user (free tier), installs the sidecar's npm deps. - Webhook management: 'hermes photon webhook register|list|delete'. - Credentials persisted under credential_pool.photon / credential_pool.photon_project in ~/.hermes/auth.json. Plugin path (not built-in) — per current policy (May 2026), all new platforms ship under plugins/platforms/. Registers itself via ctx.register_platform() + ctx.register_cli_command(), zero edits to core gateway code. Tests cover: - HMAC-SHA256 signature verification (happy path, tampered body, wrong secret, drift, missing v0 prefix, empty inputs, non-integer timestamp) - Inbound dispatch for text DMs, group ids (any;+;...), and attachment metadata markers - Deduplication window - check_requirements gating when Node is absent - Device-code flow: request, header-based token return, body-fallback token return, access_denied propagation - Project/user/webhook API clients with mocked httpx Known limitations (current Photon API): - Attachments are metadata only — no download URL yet - Outbound attachment send not wired (sidecar can add easily) - Reactions / message effects not exposed yet Docs: website/docs/user-guide/messaging/photon.md + sidebar entry.
5.3 KiB
Photon iMessage platform plugin
This plugin connects Hermes Agent to iMessage (and WhatsApp Business + future Spectrum interfaces) through Photon — a managed service that handles the iMessage line allocation, delivery, and abuse-prevention layer so users don't have to run their own Mac relay.
The free tier uses Photon's shared iMessage line pool (type: shared)
and is the path we recommend for everyone who doesn't already pay for a
dedicated number.
Architecture
┌─────────────────────────┐ HMAC-signed POSTs ┌──────────────────┐
│ Photon Spectrum cloud │ ──────────────────────► │ Hermes Agent │
│ (iMessage line owner) │ │ (Python) │
└─────────────────────────┘ JSON over loopback │ │
▲ ◄────────────────────── │ PhotonAdapter │
│ │ + aiohttp recv │
│ spectrum-ts │ │
│ SDK (Node) │ spawns + super- │
▼ │ vises ▼ │
┌─────────────────────────┐ ├──────────────────┤
│ Node sidecar │ ◄──── X-Hermes- ─ │ Node sidecar │
│ (plugins/.../sidecar) │ Sidecar-Token │ child process │
└─────────────────────────┘ └──────────────────┘
Inbound traffic is webhook-only — Hermes runs an aiohttp listener
that verifies X-Spectrum-Signature and dedupes on message.id.
Outbound traffic goes through a tiny Node sidecar that runs the
spectrum-ts SDK. Photon does not currently expose an HTTP
send-message endpoint; their own docs say:
Pass
space.idtoSpace.send(...)from a separatespectrum-tsSDK instance to reply. No public HTTP send endpoint exists today. — https://photon.codes/docs/webhooks/events
When Photon ships an HTTP send endpoint, _sidecar_send is the one
function that swaps and the sidecar disappears. The rest of the
plugin stays the same.
First-time setup
# 1. Log in via the device-code flow (opens browser)
hermes photon login
# 2. Full setup: project, user, sidecar deps
hermes photon setup --phone +15551234567
# 3. Expose your webhook URL to the public internet
# (cloudflared, ngrok, your gateway's public hostname, etc.)
# Then register it with Photon:
hermes photon webhook register https://your-host.example.com/photon/webhook
# 4. Save the signing secret it prints to ~/.hermes/.env
# as PHOTON_WEBHOOK_SECRET=...
# Photon only returns it ONCE.
# 5. Start the gateway
hermes gateway start --platform photon
Credentials
Stored in ~/.hermes/auth.json under credential_pool:
{
"credential_pool": {
"photon": [
{ "access_token": "<dashboard-bearer>", "issued_at": ... }
],
"photon_project": [
{ "project_id": "...", "project_secret": "...", "name": "Hermes Agent" }
]
}
}
The per-URL webhook signing secret is treated like an API key and
lives in ~/.hermes/.env as PHOTON_WEBHOOK_SECRET.
Configuration knobs
All env vars are documented in plugin.yaml. The most important are:
| Env var | Default | Meaning |
|---|---|---|
PHOTON_PROJECT_ID |
from auth.json | Spectrum project ID |
PHOTON_PROJECT_SECRET |
from auth.json | Spectrum project secret (HTTP Basic) |
PHOTON_WEBHOOK_SECRET |
(unset) | Signing secret returned at register |
PHOTON_WEBHOOK_PORT |
8788 | Local port for the aiohttp listener |
PHOTON_WEBHOOK_PATH |
/photon/webhook | Path under which the listener mounts |
PHOTON_SIDECAR_PORT |
8789 | Loopback port for sidecar control |
PHOTON_HOME_CHANNEL |
(unset) | Default space ID for cron delivery |
PHOTON_ALLOWED_USERS |
(unset) | Comma-separated E.164 allowlist |
Limitations (current Photon API)
- Attachments are metadata only. Inbound webhooks include the
filename + MIME type but no download URL. The plugin surfaces a
text marker (
[Photon attachment received: …]) so the agent knows something arrived, but cannot read the bytes. Photon's docs note an attachment retrieval endpoint is on the roadmap. - Outbound attachments are not supported yet. Adding them is
straightforward once the sidecar wires up
attachment(...)/space.send(attachment(...))fromspectrum-ts. - Reactions, message effects, polls — not exposed yet; the
spectrum-tsSDK supports them, and the sidecar is the natural place to add them when the agent has reason to use them.