mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-16 09:31:37 +00:00
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>
132 lines
6.2 KiB
Markdown
132 lines
6.2 KiB
Markdown
# Photon iMessage platform plugin
|
|
|
|
This plugin connects Hermes Agent to iMessage (and other Spectrum
|
|
interfaces) through [Photon][photon] — a managed service that handles
|
|
iMessage line allocation, delivery, and abuse-prevention so users don't
|
|
have to run their own Mac relay.
|
|
|
|
The free tier uses Photon's shared iMessage line pool and is the path we
|
|
recommend for everyone who doesn't already pay for a dedicated number.
|
|
|
|
## Architecture
|
|
|
|
Like Discord and Slack, Photon is a **persistent-connection** channel — no
|
|
public URL, no webhook, no signing secret. The `spectrum-ts` SDK holds a
|
|
long-lived **gRPC stream** to Photon for both directions. Because the SDK is
|
|
TypeScript-only, Hermes runs it inside a small supervised Node sidecar and
|
|
talks to it over loopback.
|
|
|
|
```
|
|
gRPC (spectrum-ts)
|
|
┌─────────────────────────┐ ◄───────────────► ┌──────────────────────┐
|
|
│ Photon Spectrum cloud │ app.messages │ Node sidecar │
|
|
│ (iMessage line owner) │ space.send() │ (plugins/…/sidecar) │
|
|
└─────────────────────────┘ └──────────┬───────────┘
|
|
GET /inbound (NDJSON) │ ▲ POST /send
|
|
inbound events ▼ │ /typing
|
|
┌──────────────────────┐
|
|
│ PhotonAdapter │
|
|
│ (Python, in gateway) │
|
|
└──────────────────────┘
|
|
```
|
|
|
|
- **Inbound**: the sidecar consumes the SDK's `app.messages` gRPC stream,
|
|
normalizes each message, and streams it to the adapter over a loopback
|
|
`GET /inbound` (NDJSON). The adapter dedupes on `messageId` and dispatches
|
|
a `MessageEvent` to the gateway. It reconnects automatically if the stream
|
|
drops; the sidecar owns the gRPC reconnect to Photon.
|
|
- **Outbound**: `send` / `send_typing` are loopback POSTs to the sidecar,
|
|
authenticated with a shared `X-Hermes-Sidecar-Token`.
|
|
|
|
## First-time setup
|
|
|
|
```bash
|
|
# One-shot setup: device login (opens browser) + project + user + sidecar deps
|
|
hermes photon setup --phone +15551234567
|
|
|
|
# Start the gateway
|
|
hermes gateway start --platform photon
|
|
```
|
|
|
|
`hermes photon setup` does, in order:
|
|
|
|
1. **Device login** (RFC 8628, `client_id=photon-cli`) — opens
|
|
`https://app.photon.codes/` for approval and stores the bearer token.
|
|
2. **Find or create** the `Hermes Agent` project on the Photon dashboard.
|
|
3. **Enable Spectrum**, read the project's `spectrumProjectId`, rotate the
|
|
project secret, and persist both.
|
|
4. **Register your phone number** as a Spectrum user (idempotent — skipped if
|
|
a user with that number already exists).
|
|
5. **Print the assigned iMessage line** — the number you text to reach your
|
|
agent.
|
|
6. **Install the sidecar deps** (`spectrum-ts`).
|
|
|
|
There is no separate `login` command; like every other Hermes channel,
|
|
onboarding goes through one setup surface. Re-running `setup` reuses an
|
|
existing token/project, so it's safe to run again to finish a partial setup.
|
|
Run `hermes photon status` to see what's configured.
|
|
|
|
## Credentials
|
|
|
|
Runtime SDK credentials live in `~/.hermes/.env` (the same place every other
|
|
channel keeps its token), and the adapter reads them from the environment:
|
|
|
|
```bash
|
|
PHOTON_PROJECT_ID=<spectrumProjectId> # the SDK's projectId
|
|
PHOTON_PROJECT_SECRET=<projectSecret>
|
|
```
|
|
|
|
Management metadata lives in `~/.hermes/auth.json` under `credential_pool`:
|
|
|
|
```jsonc
|
|
{
|
|
"credential_pool": {
|
|
"photon": [
|
|
{ "access_token": "<device-bearer>", "issued_at": ... }
|
|
],
|
|
"photon_project": [
|
|
{
|
|
"dashboard_project_id": "<dashboard id>",
|
|
"spectrum_project_id": "<spectrumProjectId>",
|
|
"project_secret": "<projectSecret>",
|
|
"name": "Hermes Agent"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
> **Note on ids.** A Photon project has two identifiers: the dashboard `id`
|
|
> (used for management API calls) and the `spectrumProjectId` (what the SDK
|
|
> authenticates with). `PHOTON_PROJECT_ID` is the **spectrum** id.
|
|
|
|
## Configuration knobs
|
|
|
|
All env vars are documented in `plugin.yaml`. The most important:
|
|
|
|
| Env var | Default | Meaning |
|
|
|---------------------------|----------------------------|--------------------------------------|
|
|
| `PHOTON_PROJECT_ID` | from .env / auth.json | Spectrum project id (SDK `projectId`)|
|
|
| `PHOTON_PROJECT_SECRET` | from .env / auth.json | Project secret |
|
|
| `PHOTON_SIDECAR_PORT` | 8789 | Loopback port for the sidecar |
|
|
| `PHOTON_SIDECAR_AUTOSTART`| true | Spawn the sidecar on connect |
|
|
| `PHOTON_DASHBOARD_HOST` | https://app.photon.codes | Dashboard API host |
|
|
| `PHOTON_HOME_CHANNEL` | (unset) | Default space id for cron delivery |
|
|
| `PHOTON_ALLOWED_USERS` | (unset) | Comma-separated E.164 allowlist |
|
|
| `PHOTON_REQUIRE_MENTION` | false | Gate group chats on a wake word |
|
|
|
|
## Limitations (current Photon API)
|
|
|
|
- **Inbound attachments are metadata only.** Inbound events carry the
|
|
filename + MIME type; the plugin surfaces a text marker
|
|
(`[Photon attachment received: …]`) so the agent knows something arrived.
|
|
The SDK exposes attachment bytes via `content.read()`/`stream()`, so
|
|
downloading them is a sidecar follow-up.
|
|
- **Outbound attachments are supported.** Images, voice notes, video, and
|
|
documents are sent via `space.send(attachment(...))` /
|
|
`space.send(voice(...))` through the sidecar's `/send-attachment`
|
|
endpoint; a caption is delivered as a separate text bubble after the media.
|
|
- **Reactions, message effects, polls** — supported by `spectrum-ts` but not
|
|
yet exposed; the sidecar is the natural place to add them.
|
|
|
|
[photon]: https://photon.codes/
|