* fix(photon): override transitive CVEs in the sidecar deps
`npm audit` flagged 7 high-severity transitive CVEs (protobufjs code injection
GHSA-66ff-xgx4-vchm + outdated @opentelemetry OTLP exporters) pulled in via
spectrum-ts -> @photon-ai/otel. npm's suggested fix downgrades spectrum-ts to a
version that targets the decommissioned spectrum host, so instead pin patched
versions via `overrides` (protobufjs 8.6.1, @opentelemetry/* 0.218.0) without
touching spectrum-ts. `npm audit` -> 0; spectrum-ts + provider still import.
* fix(photon): harden the sidecar bridge + bound the dedup cache
- constant-time sidecar control-token comparison (was `!==`, timing-attackable).
- cap the control-channel request body (2 MiB) so a compromised local peer can't
OOM the sidecar.
- wrap the inbound gRPC stream consumer in a re-subscribe loop with capped
exponential backoff + jitter — if the async iterator throws/ends it would
otherwise stop inbound forever (the adapter dedupes any replay).
- add an unhandledRejection handler so a stray rejection logs instead of killing
the process.
- dedup cache (adapter) was a true bounded LRU only for expired entries; a burst
of unique ids within the window grew it without limit. Evict oldest at the cap.
* chore: add AUTHOR_MAP entry for PhilipAD
---------
Co-authored-by: PhilipAD <philipadsouza@gmail.com>
Extend the sidecar and Python adapter to handle `voice` content
alongside `attachment`. Voice notes are inlined as base64 (same
size-cap logic), surfaced as `MessageType.VOICE`, and include an
optional `duration` field in fallback markers when bytes are
unavailable.
Drop `replyTo` from all outbound send paths and update the `/typing`
endpoint to use the documented `typing("start" | "stop")` content
builder. Adds a `stop_typing` method on the adapter to pair with
`send_typing`.
Replace raw `{ replyTo }` send options with the `spectrumReply` content
builder from spectrum-ts, which is the correct API for threading
replies.
Adds `maybeReplyContent` helper with graceful fallback to normal send
when
the reply target cannot be resolved.
Allow PHOTON_HOME_CHANNEL to accept a bare E.164 phone number or a
`any;-;+1...` DM chat GUID in addition to a Spectrum space id. Inbound
DM spaces are cached so replies resolve without a second SDK lookup,
and `photon` is added to _PHONE_PLATFORMS so send_message treats E.164
strings as explicit targets rather than falling through to channel-name
resolution.
During `hermes photon setup`, allowlist the operator's number and set
their DM as the cron home channel when those env vars are unset. Without
this, the gateway denies the operator's own messages and cron has no
default delivery target. Re-runs never overwrite hand-tuned values.
Also teaches the sidecar's `resolveSpace` to accept a bare E.164 number
as a space identifier, resolving it to the user's DM space so
`PHOTON_HOME_CHANNEL` can be set to a phone number instead of an opaque
space id.
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>
Photon now exposes attachment send (Ray Sun, photon-nousresearch), so
the Photon plugin gains outbound media to match the BlueBubbles iMessage
channel.
- sidecar: new /send-attachment endpoint wrapping space.send(attachment())
/ space.send(voice()); caption sent as a trailing text bubble.
- adapter: override send_image/send_image_file/send_voice/send_video/
send_document/send_animation. URL helpers cache to a local path first
(cache_image_from_url), file helpers pass through. Defense-in-depth
path re-validation before the path reaches the Node sidecar.
- _standalone_send (cron): send text first, then each media_file as a
/send-attachment call (is_voice -> voice builder).
- docs/README: flip the 'outbound attachments not wired' note.
CI red on three blocking checks; all addressed:
1. Windows footguns: os.killpg() flagged as POSIX-only despite the
sys.platform != 'win32' guard. Static scanner doesn't see flow.
Added the documented '# windows-footgun: ok' suppression.
2. test (3): tests/plugins/platforms/photon/__init__.py shadowed the
real plugin's __init__.py because test_plugin_platform_interface.py
looks at PROJECT_ROOT/plugins/platforms/<name>/__init__.py with
PROJECT_ROOT=tests/ (pre-existing bug in that test, made visible
by the new test directory layout). Dropping the empty test
__init__.py restores the prior NOTSET parametrize behavior.
3. CodeQL (7 alerts in new code):
- cli.py: stop printing the first 8 chars of the bearer token after
login — even prefixes are partial credentials.
- cli.py: stop printing the first 8 chars of project_secret after
setup, same reason.
- cli.py 'hermes photon webhook register': stop dumping the raw
register-webhook response (contained signingSecret) and stop
echoing PHOTON_WEBHOOK_SECRET to stdout. Write it directly to
~/.hermes/.env (0o600), preserving existing entries; fall back
to manual instructions only if the file write fails. Photon
still only returns the secret once; this just doesn't put it
in scrollback / shell history.
- cli.py setup + status: rename project_id/project_secret/token
locals to has_* booleans before printing, breaking CodeQL's
taint flow through f-string interpolations. Drop diagnostic
prints of phone / assignedPhoneNumber that flagged as
'sensitive data' false positives.
- sidecar/index.mjs: stop returning the raw error message
(potentially containing stack trace) in HTTP 500 responses;
supervisor logs the real error to stderr, client only sees
a generic 'internal sidecar error'.
Validation:
- scripts/check-windows-footguns.py --all → 0 footguns (518 files)
- tests/plugins/platforms/photon/ → 22/22 pass
- tests/gateway/test_plugin_platform_interface.py → 7/7 pass, collects
NOTSET (matches pre-PR state)
- tests/gateway/test_platform_registry.py → 50/50 pass
- node --check sidecar/index.mjs clean
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.