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. |
||
|---|---|---|
| .. | ||
| sidecar | ||
| __init__.py | ||
| adapter.py | ||
| auth.py | ||
| cli.py | ||
| plugin.yaml | ||
| README.md | ||
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. One-shot setup: device login (opens browser) + project + user + sidecar deps
hermes photon setup --phone +15551234567
# 2. 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
# 3. Save the signing secret it prints to ~/.hermes/.env
# as PHOTON_WEBHOOK_SECRET=...
# Photon only returns it ONCE.
# 4. Start the gateway
hermes gateway start --platform photon
hermes photon setup runs the RFC 8628 device-code login as its first
step — it opens https://app.photon.codes/ for approval, then
provisions the Spectrum project + iMessage line. There is no separate
login command; like every other Hermes channel, onboarding goes
through one setup surface. Re-running setup reuses an existing token
and project, so it's safe to run again to finish a partial setup.
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.