feat(gateway): add Photon Spectrum (iMessage) platform plugin
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.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Photon iMessage platform plugin
|
||||
|
||||
This plugin connects Hermes Agent to iMessage (and WhatsApp Business +
|
||||
future Spectrum interfaces) through [Photon][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.id` to `Space.send(...)` from a separate `spectrum-ts`
|
||||
> SDK 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
|
||||
|
||||
```bash
|
||||
# 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`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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(...))` from `spectrum-ts`.
|
||||
- **Reactions, message effects, polls** — not exposed yet; the
|
||||
`spectrum-ts` SDK supports them, and the sidecar is the natural
|
||||
place to add them when the agent has reason to use them.
|
||||
|
||||
[photon]: https://photon.codes/
|
||||
Reference in New Issue
Block a user