Files
signal_bridge_remote/CHANGELOG.md
AletheiaVox 7bb9d8473b fix(security): require auth on every MCP request; stop stale refreshes banning clients
- Remove the authless "sole connected phone" fallback and SB_REQUIRE_MCP_AUTH:
  an unauthenticated request no longer reaches whichever phone is online alone.
- Mcp-Session-Id is no longer a credential; the Bearer token is checked on
  every request (MCP auth spec).
- Refresh tokens live 90 days (was 30, equal to the access token, so they
  were always dead when first needed).
- A rejected refresh no longer counts toward the IP ban: a client with an
  expired token was retrying into a self-renewing ban on its own IP.
- 401s carry the RFC 9728 WWW-Authenticate discovery header.
2026-09-27 12:55:16 +02:00

140 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## Unreleased
### Security
- **Removed the authless "sole connected phone" fallback.** With
`SB_REQUIRE_MCP_AUTH=false` (the old default), any unauthenticated MCP
request was routed to whichever phone was connected, as long as it was the
only one. On a shared server that means: a stranger connects while you're
the only one online and gets your hardware. Every MCP request now needs a
valid Bearer token (OAuth or login JWT). `SB_REQUIRE_MCP_AUTH` is gone; an
old `.env` that still sets it is harmless.
- **`Mcp-Session-Id` is no longer a credential.** A session ID used to stand
in for the token on follow-up requests, so it kept working after the
token expired or was revoked. The Bearer token is now checked on every
request, as the MCP auth spec requires.
### Fixed
- **Clients no longer ban their own IP when their token expires.** Refresh
tokens lasted 30 days, the same as a 720-hour access token, and clients
only refresh once the access token is dead, so the refresh token was
always dead too. Every rejected refresh counted as a failed login, and a
client retrying on a timer reached `SB_BAN_THRESHOLD` in about half an
hour, then got banned again every time the ban lapsed. Refresh tokens now
last 90 days, and rotation issues a new one on every refresh. A rejected
refresh (the client has already proven its `client_secret`) no longer
counts toward the ban. Wrong passwords and wrong client secrets still do.
- **Disabling the governor now disables the governor.** `enabled` gated only
`Governor.check()` — the enforcement call. The heat model kept integrating
on every heartbeat, the state kept riding along on heartbeat pings, and
`list_devices` kept appending a heat/cooldown footer. With the governor
switched off, a session could still surface `⚠ Governor: COOLDOWN` and
transition the phone's own state machine to COOLDOWN while nothing was
being blocked at all — a disabled safety layer announcing a stop it would
never enforce. `tick()` now returns early when disabled and clears any
state carried over from before the toggle; `to_dict()` reports
`enabled: false` with zeroed values; the `list_devices` footer is omitted
entirely.
- **Timed commands no longer accumulate heat forever.** `current_intensity`
was cleared only by `record_stop()`, which has three callers: an explicit
`stop`, a phone-initiated emergency stop, and the dead man's switch. A
pattern that ended on its own declared duration never told the server, so
the heat model went on integrating at the last commanded intensity against
hardware that had already stopped — indefinitely. Commands now carry their
duration into `record_command()` and the intensity expires when it lapses.
`duration: 0` still means "runs until an explicit stop" and stays sticky.
For `escalate`, the expiry is `duration + hold_seconds`, and only when
`hold_seconds > 0` — with `hold_seconds: 0` it holds at peak indefinitely
by contract.
### Added
- `tests/verify_governor.py` — offline verification of the heat model:
the disabled path, timed-command expiry, and the enabled-path invariants
(cooldown trigger, cooldown exit on both heat and time, idle dissipation).
Pure logic, no server or hardware. `python tests/verify_governor.py`
- `enabled` is now included in the governor state dict, so heartbeat
consumers and `/safety/status` can tell a quiet governor from an absent
one. Existing phone clients read heartbeat fields by key lookup and ignore
unknown ones, so no client update is required.
## v1.1 — 2026-07-07
This release brings the remote server up to date with everything that
shipped in the [Android edition](https://github.com/AletheiaVox/signal_bridge_android)
since March, plus fixes found while porting. If you run a server from an
older clone, rebuild the Docker image (`docker-compose build --no-cache`)
and update your relay client — both sides changed.
### Added
- **OAuth 2.0 support.** Full flow for claude.ai custom connectors:
discovery metadata, dynamic client registration, a login page at
`/oauth/authorize`, and a token endpoint. Each user signs in with their
own account — no more sharing one token or relying on the single-phone
fallback. The fallback still works for single-user servers and can be
disabled with `SB_REQUIRE_MCP_AUTH=true`.
- **Safety governor.** Server-side heat model (intensity × time) that
forces a cooldown when a session runs hot for too long. Server defaults
via `SB_GOVERNOR_*` env vars, per-user overrides via
`GET`/`POST /safety/config`, current state surfaced in `list_devices`
and piggybacked on heartbeat pings.
- **`feature_index` on every output and pattern tool.** Multi-motor
devices (Lovense Edge, Dolce, …) can now have each motor driven
independently. Implemented in both relay clients; a bad index returns a
helpful error listing the valid ones.
- **`CHANGELOG.md`, `.env.example`, `.gitignore`, `.gitattributes`.**
### Changed
- **Neutral engineering terminology** in tool schemas and
`devices.json`, matching the Android edition. Content filters on some
LLM platforms refused tools whose schemas contained explicit anatomical
language; the reworded schemas work across providers. `list_devices`
now also shows what each output channel does.
- Duplicate same-model devices get suffixed short names (`lush`,
`lush_2`) instead of silently replacing each other.
- Relay clients no longer push an unsolicited device list after
authenticating; the server requests a scan on connect and the scan
response covers it.
### Fixed
- **MCP protocol compliance:** JSON-RPC notifications (e.g.
`notifications/initialized`) now get the bare `202` the spec requires
instead of a malformed error, and `initialize` echoes the client's
`protocolVersion` when supported. Fixes connection failures with strict
clients.
- **OAuth token endpoint** 500'd when a client POSTed the token request
as `multipart/form-data` (missing `python-multipart` dependency).
- **`governor_enabled`** now round-trips as a real JSON boolean; strict
clients (the Android app) previously couldn't re-enable the governor
after disabling it.
- **Patterns with `duration=0` silently did nothing.** They now run
until an explicit stop, matching plain commands.
- **`escalate` hold contract:** `hold_seconds` ≤ 0 holds at peak until
stopped, > 0 auto-stops after the hold — and an error mid-ramp can no
longer leave the device running at the last intensity it reached.
- **Pattern collisions:** starting a pattern cancels the previous
pattern on that device (they used to fight over the actuator), and a
direct command now supersedes a running pattern instead of being
overwritten by it.
- **Stale auto-stops:** a `duration` auto-stop from an earlier command
could fire minutes later and silently kill output a newer command had
started. Auto-stops are now tracked per channel and cancelled when
something newer takes over.
- Pattern timing now uses a monotonic clock; an NTP wall-clock jump
could stretch, truncate, or instantly end a pattern.
- Repo hygiene: removed accidentally committed compiled bytecode
(`__pycache__`) and a stray deploy tarball.
## v1.0 — 2026-03-14
Initial public release: FastAPI relay server (MCP over HTTPS, JWT auth,
rate limiting, dead man's switch), Windows/desktop relay client, and the
Termux relay for Android.