- 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.
7.3 KiB
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_AUTHis gone; an old.envthat still sets it is harmless. Mcp-Session-Idis 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_THRESHOLDin 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 itsclient_secret) no longer counts toward the ban. Wrong passwords and wrong client secrets still do. -
Disabling the governor now disables the governor.
enabledgated onlyGovernor.check()— the enforcement call. The heat model kept integrating on every heartbeat, the state kept riding along on heartbeat pings, andlist_deviceskept appending a heat/cooldown footer. With the governor switched off, a session could still surface⚠ Governor: COOLDOWNand 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()reportsenabled: falsewith zeroed values; thelist_devicesfooter is omitted entirely. -
Timed commands no longer accumulate heat forever.
current_intensitywas cleared only byrecord_stop(), which has three callers: an explicitstop, 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 intorecord_command()and the intensity expires when it lapses.duration: 0still means "runs until an explicit stop" and stays sticky. Forescalate, the expiry isduration + hold_seconds, and only whenhold_seconds > 0— withhold_seconds: 0it 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.pyenabledis now included in the governor state dict, so heartbeat consumers and/safety/statuscan 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 withSB_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 viaGET/POST /safety/config, current state surfaced inlist_devicesand piggybacked on heartbeat pings. feature_indexon 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_devicesnow 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 bare202the spec requires instead of a malformed error, andinitializeechoes the client'sprotocolVersionwhen supported. Fixes connection failures with strict clients. - OAuth token endpoint 500'd when a client POSTed the token request
as
multipart/form-data(missingpython-multipartdependency). governor_enablednow 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=0silently did nothing. They now run until an explicit stop, matching plain commands. escalatehold 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
durationauto-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.