Skip to content

Connections: switch between the local core and remote cores (remote transport stubbed) - #181

Closed
fylorn wants to merge 2 commits into
devfrom
feat/connections
Closed

fylorn wants to merge 2 commits into
devfrom
feat/connections

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Part 1 of P5 in the remote-core plan: the connection-management shell. The remote transport (Noise handshake, key from listen.control.key) is not released yet, so it sits behind one stub that P2 fills in.

What changes

Model (Rust, src-tauri/src/connection/)

  • store.rs: connections.json in the data dir — { remotes: [{ id, name, host, port, last_connected_at }], last_used, startup: "last" | "local" }. "This Mac" is built in (id local), not stored, cannot be deleted. Startup default = last used connection.
  • secrets.rs: control keys in connection-keys.json in the data dir — one code path on every platform, file created 0600 from the start (temp file with the mode, then rename), directory created private (0700 / protected DACL, private_dir.rs ported from core). No OS keychain (it prompts for the login password on macOS). Kept apart from connections.json; never returned to the webview — the edit dialog shows "saved on this computer" + Replace.
  • connector.rs: test(&Target) -> Result<ServerInfo { core_version, gateway_addr }, ConnectError> with Unreachable / Closed / WrongKey / VersionMismatch { ours, theirs } / Timeout, plus a temporary NotYetAvailable. Local = existing path. Remote does the TCP connect for real, then calls connector::handshake — the single P2 seam, currently a stub.
  • mod.rs: Link (current connection + LinkState), remote reconnect loop with backoff, commands, switch logic. ControlClient is now retargetable: all clones of AppState.control follow the current connection; the supervisor and heartbeat keep their own local client.
  • launch.rs: launch guard (launch-attempts marker; two launches in a row that never got ready → picker first) and Option/Alt-held detection (macOS, Windows; Linux has only the guard).

Behaviour

  • Remote mode stops the local core after in-flight requests finish (wait_for_quiet, now shared with the updater) and the watchdog does not restart it; switching back starts it. Never falls back to local on its own; restart_core in remote mode means "retry now".
  • core-state now describes the active connection (running:0 / unlinked for remote), so the existing resync paths (useRequests, useLive, events_dropped) handle reconnects. The event bridge follows retargets and waits while a remote is down instead of polling it.
  • Switching remounts the main UI so no data from the previous core survives.

UI (mockups ①–⑦)

  • ① Sidebar footer switcher + popover. ② Settings › Connection at the top of Settings, startup segmented control. ③ Add/edit dialog: Test connection / Save / Save and switch (always tests first), the four failure messages with next steps. ④ Switch confirmation with the "also point these clients at " checkbox. ⑤ Not-connected page after ≤ 8 s of launch screen (data pages disabled, Settings usable, retry/edit/switch to this Mac). ⑥ Version-mismatch page; mid-session disconnect banner, read-only content, one system notification. ⑦ Menubar/tray "Connection" submenu (macOS NSMenu and the Linux/Windows tray), and the launch picker window.

Left for later

  • P2: connector::handshake (and delete ConnectError::NotYetAvailable).
  • Adoption moving into lite: clients::adopted_pointing_at_local still asks core's /clients; clients::retarget_adopted_clients is a TODO hook that currently returns an error (logged, the switch itself still happens).
  • ⑧ and the remote-mode trimming.
  • The upgrade command on the mismatch page reads twcore upgrade until P6 settles it.

Checks

cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test (with twcore v0.46.0), pnpm typecheck, pnpm build, pnpm test. UI verified in the isolated preview harness (mockIPC) for local, remote connected, startup timeout, wrong key, version mismatch, mid-session loss/recovery, the switch flow and the picker.

🤖 Generated with Claude Code

fylorn and others added 2 commits September 24, 2026 23:37
…ransport stubbed)

The connection-management shell from the remote-core plan (P5, part 1):

- Connection list persisted by the app itself (connections.json in the data
  dir): built-in, undeletable "This Mac" plus remote entries. Keys live in the
  OS keychain (keyring crate), never in the file and never sent back to the UI.
- One connector seam (connection::connector): test/connect returns ServerInfo
  or a ConnectError (unreachable / closed / wrong key / version mismatch /
  timeout). The remote handshake is a single stub, connector::handshake, that
  P2 fills in with the Noise handshake; until then it answers "not available
  yet" after the TCP connect.
- ControlClient is retargetable; every clone follows the current connection.
  In remote mode the local core stops once in-flight requests finish and the
  watchdog does not restart it; switching back starts it. Never falls back to
  local on its own.
- Launch screen waits at most ~8 s, then the window opens in a "not
  connected" state (data pages disabled, Settings usable, retry with backoff,
  edit, switch to this Mac). Version-mismatch page. Mid-session disconnect
  shows a banner, makes the content read-only, raises one notification and
  reconciles through the existing core-state/events_dropped path.
- Launch guard: two launches in a row that never got ready, or holding
  Option/Alt at launch, show a small connection picker first.
- Sidebar switcher, Settings > Connection section, add/edit dialog with
  test / save / save and switch, switch confirmation with the "also point
  these clients at the server" option (clients::retarget_adopted_clients is
  a TODO hook until adoption moves into lite).
- Menubar/tray "Connection" submenu on all platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the keychain prompts for the login password for this item (again
after each update of an unsigned app), which is not worth it for a key that
can be copied from the server again. There is now one code path on every
platform: connection-keys.json in the data directory, created 0600 from the
start (temp file with the mode, then rename), in a directory created private
(0700 on unix, protected DACL on Windows; ported from core's private_dir).

Kept apart from connections.json so the frequent list writes never touch the
secret and the code that reads the list cannot see it. Keys still never go
to the webview. The keyring crate and all keychain copy are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Sep 24, 2026
…mode UI (#182, includes #181)

* Adopt clients, edit MCP and scan client configs in the app instead of core

Client adoption, MCP editing, the static scan and the file watcher change
files on this machine, so they now run in the desktop app. The tw-adopt and
tw-scan crates move here from core (src-tauri/crates/, one workspace with the
app; the unused tw-config dependency is dropped).

- New Tauri commands replace the control-plane endpoints: list_clients,
  plan_adopt, adopt_client, plan_restore, restore_client, diagnose_client,
  prepare_client_key, mcp_targets, plan_mcp, apply_mcp, scan_clients,
  gateway_base, plus delete_key and rotate_key (the adopted-client check and
  the key sync need this machine's files). restore_all no longer asks core.
- Core is still asked for two things only: the gateway port (Overview) and
  keys (Keys, ClientKey). The gateway host is a single function so remote
  mode can swap it; retarget_adopted() repoints every adopted client.
- The watcher runs in the app: `local-event` (clients_changed / scan_alert)
  to the webview, scan alerts into the notice bus; the first scan is a
  baseline. Core's own ClientsChanged/ScanAlert are ignored.
- Types for these commands live in src-tauri/src/wire.rs and generate
  src/generated/lite-api.ts; names identical to tw-api.ts are imported.
- Message codes the app emits itself are listed in src-tauri/msg-codes.txt
  (generated from source); translations are checked against it plus core's.
- The webview endpoint whitelist drops the moved endpoints, and DeleteKey,
  RotateKey and ClientKey now go only through Rust commands.

Works against the pinned core v0.46.0 (its endpoints just go unused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Connections: switch between the local core and remote cores (remote transport stubbed)

The connection-management shell from the remote-core plan (P5, part 1):

- Connection list persisted by the app itself (connections.json in the data
  dir): built-in, undeletable "This Mac" plus remote entries. Keys live in the
  OS keychain (keyring crate), never in the file and never sent back to the UI.
- One connector seam (connection::connector): test/connect returns ServerInfo
  or a ConnectError (unreachable / closed / wrong key / version mismatch /
  timeout). The remote handshake is a single stub, connector::handshake, that
  P2 fills in with the Noise handshake; until then it answers "not available
  yet" after the TCP connect.
- ControlClient is retargetable; every clone follows the current connection.
  In remote mode the local core stops once in-flight requests finish and the
  watchdog does not restart it; switching back starts it. Never falls back to
  local on its own.
- Launch screen waits at most ~8 s, then the window opens in a "not
  connected" state (data pages disabled, Settings usable, retry with backoff,
  edit, switch to this Mac). Version-mismatch page. Mid-session disconnect
  shows a banner, makes the content read-only, raises one notification and
  reconciles through the existing core-state/events_dropped path.
- Launch guard: two launches in a row that never got ready, or holding
  Option/Alt at launch, show a small connection picker first.
- Sidebar switcher, Settings > Connection section, add/edit dialog with
  test / save / save and switch, switch confirmation with the "also point
  these clients at the server" option (clients::retarget_adopted_clients is
  a TODO hook until adoption moves into lite).
- Menubar/tray "Connection" submenu on all platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Copy the gateway address from the menu bar without asking core for the clients list

The menu bar's copy-address action still read gateway_base from the /clients
endpoint, which core is dropping. It now uses the same function as the
clients and keys pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Store remote connection keys on disk, not in the OS keychain

On macOS the keychain prompts for the login password for this item (again
after each update of an unsigned app), which is not worth it for a key that
can be copied from the server again. There is now one code path on every
platform: connection-keys.json in the data directory, created 0600 from the
start (temp file with the mode, then rename), in a directory created private
(0700 on unix, protected DACL on Windows; ported from core's private_dir).

Kept apart from connections.json so the frequent list writes never touch the
secret and the code that reads the list cannot see it. Keys still never go
to the webview. The keyring crate and all keychain copy are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remote mode: say which machine each page changes, trim what only works locally

Following mockup ⑧ and the remote-mode table in the plan:

- Clients and MCP pages carry a fixed note that they change this Mac's
  files, pointing clients at the server's gateway; the server's clients
  are not affected.
- Adopted clients still pointing at the local gateway are flagged on their
  row ("points to the local gateway") and above the table, with a one-click
  "Point at <server>"; what was and was not repointed is listed per client.
  The switch-confirm dialog shows the same report when some failed.
- Settings split into "the app on this Mac" and "configuration on <server>"
  (listen, retention, and a note that the remote control listener and key
  can only be changed on the server). The diagnostics bundle is hidden; About
  names the connection and the server's core version; uninstall says the
  server is not affected. Local mode keeps its layout.
- ChatGPT sign-in offers only the device code while remote (the browser
  callback can only reach the machine running core); Z.ai is unchanged.
- ${NAME} in upstream headers and the "system proxy" choice say they are
  the server's environment.
- The sidebar and the test result show the gateway clients should use: the
  host the app dialled plus the server's gateway port, not its bind address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn

fylorn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Landed as part of #182, which contained this branch in full (merged together with the adoption wiring).

@fylorn fylorn closed this Sep 24, 2026
@fylorn
fylorn deleted the feat/connections branch September 24, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant