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

7.3 KiB
Raw Permalink Blame History

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 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.