Repository navigation
Connect OpenBot Desktop to an OpenBot that runs somewhere else - #1702
Draft
davidmckayv wants to merge 11 commits into
Draft
davidmckayv wants to merge 11 commits into
davidmckayv wants to merge 11 commits into
Conversation
…ewhere Connected, nothing of OpenBot runs on this computer, so Settings, Computer shows where the app is connected, Sign out, and Use OpenBot on this computer instead, in place of the local services, memory and install folder. The app's version, updates and log folder stay. The page holds no sign-in there, so every Sign out asks the desktop app, and a sign-in that stopped working returns to the desktop app's setup. A desktop app from before it could connect is read as OpenBot on this computer, and a browser is unchanged.
The first screen's OpenBot for Business choice can now be picked. It asks for the address of the OpenBot a company runs in place of what this computer needs and Install, then signs in through the browser. Nothing is installed or started on this computer for it. The booking link moves to that screen, for a company that has no OpenBot yet. OpenBot Cloud is still shown as coming soon, and Self-hosted is unchanged. Setup asks the shell where OpenBot runs before anything else. A saved connection opens that OpenBot, or shows the first screen with the address filled in and the reason the sign-in ended, and none of the checks, downloads or starts of an install on this computer run. A shell that does not know the question is an install on this computer, as before. What goes wrong is said in the shell's own sentence. The screen reads only its kind to choose what to offer: the field to fix, Try again, Sign in again, or opening that OpenBot in the browser. Use OpenBot on this computer instead goes back, and switches a saved connection first.
…s computer A person who already runs OpenBot here had no way back to the choice of where it runs. Settings, Computer now has a Connection row that, after a confirm, stops OpenBot here and opens setup at that choice. It is drawn only by a desktop app that can connect.
… this computer A connection that didn't work is not a failed install. Every connection problem that is not about the address itself now shows the shell's sentence in the calm notice, with no failure heading and no troubleshooting logs, and the way on is the screen's button: Try again, Sign in again, or Open in browser beside Connect. The connect screen no longer scrolls at the window's default size. The failure card is as it was for setup on this computer. Setup also asks the shell whether the person chose, in Settings, to pick where OpenBot runs. When they did, the first screen shows whichever way OpenBot runs today. On a computer that already runs OpenBot, nothing starts while the choice shows, and Continue goes on as any relaunch does.
OpenBot Desktop could only run OpenBot on the computer it is installed on. This adds what it takes to show an OpenBot that runs somewhere else instead, as a library module of its own (desktop/src-tauri/src/connected) that nothing calls yet. choice: which of the two the person chose, in connection.json in the app's configuration folder, with the address and the organisation picked there. No file means on this computer. address: a typed address kept as scheme and host (https, or http for 127.0.0.1 and [::1]; no scheme is taken as https), and two public questions asked of it: what it offers and how an app signs in to it (RFC 8414). Each way that can go wrong is one kind with its own sentence: not an address, unreachable, not an OpenBot, one person's OpenBot on its own computer, a release from before apps could sign in. A redirect to another host is not followed, and metadata that names another host is refused. sign_in: OAuth 2.1 authorization code with PKCE (S256) as the public client openbot-desktop-oauth, with a listener on 127.0.0.1 at a port the system picks and a redirect of exactly /callback. The verifier, the state and the callback's first line use what provider_oauth already has (its random and its challenge are now shared, unchanged). The wait ends on the code, a refusal, Cancel or ten minutes. After the exchange the OpenBot is asked /api/me with the new token; a 401 there means it cannot serve the app. Refresh tokens rotate, so one refresh runs at a time and the new token is stored before the old one is let go. Sign-out revokes both tokens (RFC 7009) and forgets the sign-in. credentials: the refresh token in the system's credential store through keyring 4.2 (Keychain, Credential Manager, the Secret Service), under the app's identifier and the address. The access token stays in memory. Nothing token-shaped is written to a file or a log, and the log's redaction now also covers OpenBot's own token prefixes and the relay's cookie. relay: an HTTP/1 server on 127.0.0.1 only, at a port the system picks, that passes each request and WebSocket upgrade on to the one typed address with the access token added. It answers only its own Host, only requests that carry the secret made for that run (given to the window as an HttpOnly, SameSite=Strict cookie at an entry address that is never forwarded), and no request from another site. Absolute request targets and CONNECT are refused and redirects are handed back, rewritten onto the relay when they point at the typed address. Origin, Cookie, Referer, Forwarded and X-Forwarded-* are dropped on the way out and Set-Cookie on the way back. Bodies stream both ways. The organisation the person picked is named in a header on every request, set by the shell and never taken from the page. A 401 to a request with no body leads to one refresh and the request again; a refused refresh says the session ended, once, and the relay stops. The tests run against a stand-in OpenBot that answers the same addresses the same way: the metadata document, a form-encoded token endpoint with the client's id in the body, the verifier really checked, a code that burns on a wrong verifier, rotating refresh tokens where a replayed one answers invalid_grant, revocation, and a redirect that must be http://127.0.0.1:<port>/callback. The credential store in tests is keyring's own in-memory one. New crates: keyring and keyring-core; tokio, hyper, hyper-util, http-body-util, bytes and futures-util, which reqwest and Tauri already build; tokio-tungstenite for the tests' WebSocket.
The setup window and OpenBot's own page can now ask the shell to connect to an OpenBot at an address, and the shell behaves accordingly while it is connected. On this computer, nothing changes. For the setup window: connection, check_openbot_address, connect_to_openbot, cancel_connect, show_connected, sign_out_of_openbot, use_this_computer and take_connection_notice. A failure carries a kind beside its sentence so the window can choose what to offer. Connecting checks the address, signs in through the person's browser, stops an OpenBot running on this computer the way Stop does, starts the relay and points the window at it; the choice is kept only once that has worked. When a sign-in ends while OpenBot shows, the relay stops, setup comes back and the sentence waits for it. For OpenBot's page, through the openbot-desktop plugin: connection, sign_out, change_connection and set_organization, each with a permission of its own granted to the relay's origin as the others are. change_connection also works on this computer: it stops OpenBot as Stop does and asks setup to open at the choice. While connected, status answers the app's version, build and log folder without asking an engine, and restart, stop, intelligence, ai_connection and change_ai_connection are refused. The relay's origin is trusted the way a local OpenBot's is, so its page is OpenBot's own page to the window: the overlay title bar, the theme, notifications, dictation, zoom, and links to anywhere else (the typed address included) opening in the browser. While the choice is to be connected, nothing of an installation on this computer is started, read or written: the engine is not woken, nothing is downloaded or made ready, Start, Show and the upgrade are refused, the installation's folder is not looked for at launch, and Quit only stops the relay. The tray and the menu carry Show Window, Open Log Folder, Sign Out and Quit. The setup window's connect_openbot step is one the shell's telemetry knows; its events carry the step's name and never the address. docs/desktop-install.md says what the choice is, how sign-in and the relay work, and what is not there while connected.
…runtime after sign-in The boundary check found the new addresses Desktop's native side writes by hand: the deployment question, OAuth's discovery document and the check after sign-in. Each is now listed with its rule. The check after sign-in asks the runtime's own address, which is what the app's screens need from an app's sign-in and an address that stays, in place of /api/me. The test stand-in's file name now ends in _tests.rs so the scan reads it as a test.
The relay added the access token and the organisation header to every request it passed on. The same host name can serve things that are not OpenBot's: on a company's cluster a socket at a path outside OpenBot's own goes straight to another service, which checks a join token of its own. The page reaches that socket through the relay too, so the relay was handing the person's OpenBot sign-in to a service that has no business seeing it. The token and the organisation header are now added only to requests and WebSocket upgrades whose path is /api, starts with /api/, or starts with /.well-known/. Every other path (the app's pages and files, /client/websocket and anything else) is passed on with neither. An Authorization or organisation header the page sent is still dropped on every path, and the rest of the relay's rules (its own Host, the window's cookie, no other site, the stripped headers, the one address) are the same for every path. A request that carried no sign-in asks for no token, so it waits on no refresh, and a 401 to one is somebody else's refusal: it is handed back as it is and uses no refresh token. The stand-in OpenBot serves a socket at /client/websocket that takes no OpenBot token, a file at /assets/app.js, and a path that answers 401 for reasons of its own.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
OpenBot Desktop could only open the OpenBot it installs on the same computer. A person whose company runs OpenBot had no way to use the Desktop app with it.
https://openbot.example.com).openbot-desktop-oauth, with a loopback redirect on127.0.0.1and a port chosen per sign-in.keyringcrate, never in a file, and the log redaction covers the token shapes. The access token is held in memory, refreshed before it expires (refresh tokens rotate, so refresh is single-flight) and revoked at sign-out.127.0.0.1on a port chosen per run. It forwards requests and WebSocket upgrades to the one typed address and nothing else, adds the sign-in, and removesOrigin,Cookieand forwarding headers. It answers only the window: any otherHost, a foreignOrigin, or a request without the per-run secret cookie the app gives the window is refused and nothing is forwarded. The screens come from the OpenBot it is connected to, so they match its release.set_organization), remembers it, and adds it as theOpenBot-Organizationheader. The page starts calling it once the organization switcher is onmain; until then no header is sent and the person works in their own organization.What is not available while connected, and what happens instead, is listed in
docs/desktop-install.md: starting, stopping, restarting and updating OpenBot's services; the local Intelligence and its sign-in; model keys kept on this computer; local folders for Bots; the Recorder's native host finding a local OpenBot. None of it is removed from OpenBot on this computer.A desktop app from before this change, a browser, and OpenBot on this computer behave as they did.
Where it runs
connection.jsonin the app's config folder, no secret) and the refresh token in the system's credential store. Nothing on the server.127.0.0.1only: the relay for the life of a connected session, and the sign-in callback for the length of a sign-in. Nothing on the server.Boundary and audit
Changelog
CHANGELOG.mdunderUnreleased.Proof
How each test was seen failing: the set-up and Settings tests were written first and seen red. The Rust tests for the relay, sign-in, address check, wiring and log redaction were written with the code and then each seen failing by taking the behaviour out (a guard removed, a header left in), and passing again with it back. The tests of the saved choice, the credential store and the menus were not broken on purpose.
Rust (
cargo test --locked --lib --bins,cargo fmt --check,cargo clippy --all-targets -- -D warnings, on macOS):127.0.0.1and two relays get different ports; anotherHost, a foreignOrigin, a missing or wrong secret cookie, an absolute request target andCONNECTare refused and nothing reaches the OpenBot; the OpenBot sees the sign-in and the organization header and noOrigin,Cookieor forwarding header;Set-Cookiedoes not come back; a streamed answer arrives in pieces; a WebSocket echoes both ways; an expired sign-in is refreshed once and the request retried; a refused refresh ends the session.state; consent denied and Cancel; rotation with the new token stored; two refreshes at once making one request; sign-out revoking and then forgetting.Set-up screens and Settings (
bun test, typecheck, lint, format): choosing connect, each failure kind and what it offers, cancel, launch signed in and with an ended sign-in, going back to this computer, the choice reached from Settings; Settings, Computer connected, local, with an earlier desktop app, and in a browser; Sign out through the Desktop app when connected and the web sign-out otherwise.Looked at in headless Chromium and headless WebKit, light and dark, at the window's default size and a small one (set-up), and at 1440 and 700 wide (Settings): the first screen before and after, the connect screen in each state, and Settings, Computer in each mode.
Not proven yet, and needed before this leaves draft: the app itself on a Mac and on Windows from an Internal build, connected to a real OpenBot (the Keychain and Credential Manager, the browser hand-off, the tray, a conversation with a real answer, switching back to OpenBot on this computer with its data intact).