# Photon iMessage Setup

Connect Hermes to iMessage through [Photon](https://photon.codes/), a managed service that handles the Apple iMessage relay.

## Architecture

Photon uses a persistent gRPC stream (spectrum-ts SDK) — no webhook, no public URL, no signing secret. Hermes runs a Node sidecar that talks to Photon's Spectrum Cloud; the Python adapter communicates with the sidecar over loopback.

## Prerequisites

- **Photon account** at [app.photon.codes](https://app.photon.codes/) (free tier works)
- **Node.js 18.17+** on PATH
- **A phone number** that can receive iMessage (for account binding)
- No public URL or tunnel needed

## Full Setup Flow

The `hermes photon setup` command runs 5 automated steps:

```
[1/5] Device login       → OAuth 2.0 device-code flow, stores bearer token
[2/5] Create project      → Finds or creates "Hermes Agent" project
[3/5] Spectrum credentials → Rotates project secret, stores in ~/.hermes/.env
[4/5] Register phone      → Registers your number as a Spectrum user
[5/5] Install sidecar     → npm install inside plugins/platforms/photon/sidecar/
```

## Headless Server Setup

On a headless server (no display):

```bash
PYTHONUNBUFFERED=1 hermes photon setup --phone +1626XXXXXXX --no-browser
```

The `--no-browser` flag prints the device-login URL instead of trying to open a browser. `PYTHONUNBUFFERED=1` ensures the URL and code appear in captured output (otherwise buffering can make the CLI appear to hang with no output).

**What the user sees:**
```
┌─ Photon device login ────────────────────────────────────────
│  Open this URL:  https://app.photon.codes/sign-in/device?user_code=XXXX
│  Enter the code: XXXX
│  (waiting for approval — Ctrl-C to cancel)
└──────────────────────────────────────────────────────────────
```

**Action required:** Open the URL on a phone or any device, enter the code, and approve. The CLI polls every 5 seconds and continues automatically once approved.

## Verifying Setup

```bash
hermes photon status
```

Expected output when fully configured:
```
Photon iMessage status
──────────────────────
  device token        : ✓ stored
  dashboard project   : 3c90c3cc-...   (UUID)
  spectrum project id : sp-...         (Spectrum project id)
  project secret      : ✓ stored
  my number           : +1626XXXXXXX   (your phone)
  assigned number     : +1XXXXXXXXX   (agent's iMessage line)
  node binary         : /usr/bin/node
  sidecar deps        : ✓ installed
  telemetry           : off
```

## Starting the Gateway

```bash
hermes gateway start
```

Expected photon-specific log line:
```
[photon] connected — sidecar on 127.0.0.1:8789, streaming inbound over gRPC
```

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `sidecar deps : ✗` | `hermes photon install-sidecar` |
| `device token : ✗ missing` | Re-run setup to log in |
| `No iMessage line assigned` | Check [Photon dashboard](https://app.photon.codes/) |
| Sidecar won't start | Verify `node --version` ≥ 18.17 |
| CLI produces no output with --no-browser | Prepend `PYTHONUNBUFFERED=1` |
| "Unauthorized user" on your own messages | Check `PHOTON_ALLOWED_USERS` in `~/.hermes/.env` |

## User Authorization

Three models (same as other Hermes channels):

1. **DM Pairing** (default) — unknown numbers get a pairing code. Approve: `hermes pairing approve photon <CODE>`
2. **Pre-authorize** — set `PHOTON_ALLOWED_USERS=+15551234567,+15559876543` in `~/.hermes/.env`
3. **Open access** (dev only) — set `PHOTON_ALLOW_ALL_USERS=true`

## Phone Number Format

Photon validates phone numbers against `^\+[1-9]\d{6,14}$` (E.164). **Telegram redacts the middle digits with `****`**, so if the user types `+16264836412`, you may see `+162****6412` in the message. Passing a redacted number to `register_user_if_absent()` raises `ValueError: phone_number must be E.164`.

**Fix:** Ask the user for the raw digits and construct the E.164 string yourself. The user sees their own number without redaction.

## Enabling Photon in Gateway Config

After setup is complete, enable the platform in the gateway via:
```bash
hermes config set gateway.platforms.photon.enabled true
```
Direct editing of `~/.hermes/config.yaml` is blocked by the agent's soft guard.

## Gateway Restart Requirement

After enabling Photon (or any new platform), the gateway must be restarted:
```bash
# From a separate shell — NOT from inside the gateway session:
systemctl --user restart hermes-gateway
```
**Cannot run this from inside the gateway process** — it kills the gateway (and the current conversation) via SIGTERM propagation.

## Free Tier Limits

- 5,000 messages per server per day
- 50 new-conversation initiations per shared line per day
| Increases available via help@photon.codes

## Agent-Driven Setup (Two-Phase Pattern)

When an AI agent is performing the setup, the CLI's blocking device-login poll creates a problem: the agent can't hold a foreground process while waiting for human approval. Use this pattern:

### Phase 1 — Generate device code (foreground)

```python
import sys, json, os
sys.path.insert(0, os.path.expanduser('~/.hermes/hermes-agent/src'))
sys.path.insert(0, os.path.expanduser('~/.hermes/hermes-agent'))
from plugins.platforms.photon import auth as photon_auth

code = photon_auth.request_device_code()
target = code.verification_uri_complete or code.verification_uri
print(f"URL: {target}")
print(f"CODE: {code.user_code}")

# Serialize so background poller can resume
code_dict = {
    "device_code": code.device_code,
    "user_code": code.user_code,
    "verification_uri": code.verification_uri,
    "verification_uri_complete": target,
    "expires_in": code.expires_in,
    "interval": code.interval,
}
with open('/tmp/photon_device_code.json', 'w') as f:
    json.dump(code_dict, f)
```

### Phase 2 — Poll and continue setup (background)

Launch as a background terminal process that:
1. Reads the saved device code from `/tmp/photon_device_code.json`
2. Calls `photon_auth.poll_for_token(code)` to wait for user approval
3. Validates and stores the token: `photon_auth.store_photon_token(token)`
4. Runs remaining setup (project → Spectrum credentials → phone registration → sidecar install)

Reconstruct the `DeviceCode` object from saved data:
```python
code = photon_auth.DeviceCode(
    device_code=data["device_code"],
    user_code=data["user_code"],
    verification_uri=data["verification_uri"],
    verification_uri_complete=data.get("verification_uri_complete"),
    expires_in=int(data.get("expires_in", 1800)),
    interval=int(data.get("interval", 5)),
)
```

Validate and store after polling succeeds:
```python
from plugins.platforms.photon.auth import _DeviceTokenCandidate
token = photon_auth._validated_dashboard_token(
    [_DeviceTokenCandidate(source="poll", token=token)]
)
photon_auth.store_photon_token(token)
```

### Full setup continuation (after token)

```python
phone = "+162****6412"
name = "Hermes Agent"

# Find or create project
dashboard_id = photon_auth.load_dashboard_project_id()
if not dashboard_id:
    existing = photon_auth.find_project_by_name(token, name)
    if existing and existing.get("id"):
        dashboard_id = existing["id"]
    else:
        created = photon_auth.create_project(token, name=name)
        dashboard_id = created.get("id")

# Spectrum credentials
sid, secret = photon_auth.load_project_credentials()
if not sid:
    secret = photon_auth.regenerate_project_secret(token, dashboard_id)
    photon_auth.store_project_credentials(
        spectrum_project_id=dashboard_id, project_secret=secret,
        dashboard_project_id=dashboard_id, name=name
    )

# Register phone
user, created = photon_auth.register_user_if_absent(sid, secret, phone_number=phone)
agent_number = photon_auth.user_assigned_line(user)

# Auto-allowlist
from hermes_cli.config import get_env_value, save_env_value
for key in ("PHOTON_ALLOWED_USERS", "PHOTON_HOME_CHANNEL"):
    if not get_env_value(key):
        save_env_value(key, phone)

# Sidecar install
from plugins.platforms.photon.cli import _install_sidecar
rc = _install_sidecar()
```

### Why this pattern instead of `hermes photon setup`

- CLI output may buffer invisibly in tool-captured stdout — the URL and code never appear
- The CLI blocks on `poll_for_token()` until approval, which can timeout in non-interactive contexts
- Splitting the flow lets the agent clearly present the URL/code, then delegate the long poll to a background process
- The Python API (`photon_auth` module) is the reliable fallback when CLI output is invisible

## Credential Storage

| What | Where |
|------|-------|
| Runtime SDK creds | `~/.hermes/.env` (`PHOTON_PROJECT_ID`, `PHOTON_PROJECT_SECRET`) |
| Management tokens | `~/.hermes/auth.json` under `credential_pool.photon` |
| Phone numbers | `~/.hermes/auth.json` under `credential_pool.photon_user` |
