Skip to content

feat(desktop): open bots on other computers in the account - #336

Open
plotarmordev wants to merge 31 commits into
leepokai:devfrom
plotarmordev:feat/4-open-upstream-pr-desktop-app-opens-bots-
Open

plotarmordev wants to merge 31 commits into
leepokai:devfrom
plotarmordev:feat/4-open-upstream-pr-desktop-app-opens-bots-

Conversation

@plotarmordev

@plotarmordev plotarmordev commented Nov 4, 2026 •

Copy link
Copy Markdown

Summary

The desktop app can now open bots on the other computers in its account, the same way the phones already do, with no SSH setup or QR pairing.

  • In Settings → Computers → Other computers in your account, Ask for access on an online computer runs the phones' access request and shows its 6-digit code; Cancel request withdraws it, even before the code shows. Once that computer approves, its bots join the sidebar and Open on its row opens them.
  • This builds on the relay channel and access request that dev now has for Codync Cloud computers (apps/desktop/src/main/channel/, main/cloud-access.ts):
    • useCloudAttachments opens every approved account computer over the channel, not only cloud ones, except this computer and any an SSH tunnel already reaches (renderer/lib/relay-plan.ts). A computer that refuses this device reads No access, the account is listed again, and its bots leave the sidebar.
    • The access request reveals the device's nonce once the computer answers and sends the 6-digit code to the window (sasCode in main/channel/crypto.ts, checked against the shared relay vectors); cloud:cancelAccess withdraws it. Codync Cloud computers still ask by themselves; renderer/store/account-computers.ts keeps the Settings request state for the others.
    • Outgoing frames are paced to the relay's per-link budget (main/channel/relay-pacer.ts), and the relay's byte budget is 12 MiB per 10 s so one message at the host's 8 MiB cap (about 10.7 MiB once sealed) fits (cloud/src/relay-wire.ts, with a test in cloud/test/relay.test.ts).
    • A crashed or reloaded page's channel streams close in the main process.
  • Docs: docs/architecture/desktop-app.md, connections.md, docs/reference/remote-relay.md, docs/guides/accounts-and-ssh.md, docs/plans/require-sign-in.md and the user manual page web/content/docs/remote-access.mdx.

Other clients: the iPhone and Android apps already open account computers over this channel, so this brings the desktop app level with them, and they don't change. The terminal UI has no account computers. The host's track now accepts the phones' access_requested and access_request_failed (reason: denied/expired/cancelled) from the desktop app too (host/src/analytics/event.rs, analytics/reported.rs, docs/reference/analytics-events.md). It bumps the version to 2.45.0 (2.44.1 is tagged).

Validation

On macOS, with current dev merged in (host and cloud on 31022e9f; desktop again on 578cd7da, which changes only apps/desktop and docs):

  • cd host && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test: all passed (847 unit tests, plus the integration suites).
  • cd apps/desktop && npm ci && npm run typecheck && npm test: 495/495, including the new and updated tests: shared/cloud-access.test.ts (the code reaches the window only after the reveal; Cancel withdraws the request before or after the code), renderer/lib/account-computers.test.ts, renderer/lib/relay-plan.test.ts, main/channel/relay-pacer.test.ts and main/channel/relay-channel.test.ts (a call that times out behind a full relay window is never sent; the handshake deadline starts when hello leaves), main/channel/crypto.test.ts (the 6-digit code matches the protocol vectors), shared/preload-streams.test.ts and renderer/lib/refresh.test.ts.
  • cd cloud && npm ci && npm test && npm run typecheck: 196/196, typecheck clean.

Not run: Linux (Lima codync-linux) and Windows (UTM), because neither VM is installed here.

What's New: iPhone

zh-Hant

en-US

What's New: Android

zh-Hant

en-US

The main process now speaks the phones' end-to-end encrypted channel
(hello/welcome, sealed frames) through the account's relay, signed with
this install's device key, and runs the 6-digit access request. Checked
against the shared relay vectors and a host played in the test.
An account computer that approved this install opens as a store
through the relay, so its bots join the sidebar with no SSH setup or QR
pairing. Rows ask for access, show the code, and open the computer once
approved. An SSH tunnel to the same computer still wins.
- A request I cancel before the computer shows its code is deleted
  there too, instead of staying on its screen until it expires.
- A computer that refuses this device reads No access and stops
  retrying, as on the phones; a lapsed lease still retries.
- A computer that restarts mid-link fails its calls so the next one
  reconnects, rather than calling it offline.
- A channel closes once its computer's store goes, is opened once for
  concurrent first calls, and is signed only when opened.
- The relay signer refuses when signed out instead of keying an empty
  user.
- An account computer's channel closes once an SSH tunnel takes it
  over or it turns out to be this computer, not only when its grant
  goes; only a revoked one loses its store.
- A stream I close while its channel is still opening never
  subscribes, so the computer doesn't keep sending its events.
The connections plan, the relay reference and the sign-in plan still said
the desktop app has no encrypted channel to account computers.
Upstream already bumped to 2.38.1, ahead of the latest tag, so this change
ships in that release; my 2.39.0 bump is dropped and the connections doc
says 2.38.1.
@github-actions github-actions Bot added enhancement New feature or request P3: low Nice to have: features, questions, polish. labels Nov 4, 2026
@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The latest changes appear safe to merge; both numbered findings are addressed, with no new actionable issue found.

Summary

Type: feature | Areas: desktop, host, relay, web, docs | Priority: P3: low

The desktop app can request access to other account computers and open their bots through the encrypted relay.

  • Desktop users can ask other account computers to approve access.
  • Approved account computers bring their bots into the desktop app.
  • Relay links pace outgoing messages and fit a large sealed payload.
  • Remote streams close when their page or renderer process goes away.
  • Desktop and host release metadata now uses version 2.45.0.

Diagram

sequenceDiagram
  participant Desktop
  participant Cloud
  participant Computer
  Desktop->>Cloud: Request access with nonce commitment
  Computer->>Cloud: Answer with host nonce
  Desktop->>Cloud: Reveal device nonce
  Desktop->>Desktop: Show six-digit code
  Computer->>Cloud: Approve matching code
  Desktop->>Cloud: Refresh approved computers
  Desktop->>Computer: Open encrypted channel through relay
Loading

Reviews (14) · Last reviewed commit: "fix(desktop): never send a relay call th..." · Reviewed by Greptile

Comment thread apps/desktop/src/preload/index.ts Resolved
Comment thread apps/desktop/src/main/channel-crypto.ts Resolved
A reload only closed the page's local streams, so its relay subscriptions
stayed open at the computer, could feed the reloaded page (whose stream ids
started over from s1) and used up the host's subscription limit.

- The preload closes every open stream on unload, local and remote, and
  gives each stream a random id, so nothing from a page before reaches one
  of the next page.
- The main process keys relay streams per window and cancels a crashed
  page's, which never unloads.
The relay channel refused messages over 1 MiB, so a 5 MiB icon (about
6.7 MiB as base64 in JSON) failed. The host accepts 8 MiB from an approved
device (host remote::channel MAX_INBOUND).
The desktop app now reports access_requested and access_request_failed
(denied, expired or cancelled) through the host's track, as the phones
already do. Approval stays device_paired, from the host.
A request withdrawn while its status poll was on the way could still end
as denied or expired and count a second access_request_failed. The poll
now drops a status that comes back after the request was withdrawn.

The store's tests check the request, denial, expiry and cancellation
events, and that a cancel during a poll counts once. Two comments now
say what the code does: streams have random ids, and the relay channel
takes 8 MiB.
@plotarmordev

plotarmordev commented Nov 4, 2026 •

Copy link
Copy Markdown
Author

I addressed Greptile's review in 4 commits (ac161b77, 404044c8, aa78055c, 422eee22):

  • P1 preload/index.ts: streams survive a reload. The preload now closes every open stream on pagehide, relay and local alike, and names each stream with a random id, so a reloaded page never takes an old stream's id. The main process keys relay streams per window and cancels a crashed page's streams on render-process-gone, since a crashed page never unloads. shared/preload-streams.test.ts and a crash test in shared/account-channel.test.ts both failed before the fix.
  • P2 channel-crypto.ts: 1 MiB outbound cap. I raised it to 8 MiB, the host's limit for an approved device (MAX_INBOUND in host/src/remote/channel.rs, sized for a 5 MiB icon in base64 JSON). The test now checks that an icon-sized message seals and that 8 MiB + 1 is refused.
    • Correction: this alone didn't make 5 MiB icons work. On the two-computer setup, the relay closed the link with 4008 rateLimited: its per-link budget of 8 MiB per 10 s counts sealed base64url frames, and a message at the 8 MiB cap is about 10.7 MiB of them. I raised that budget to 12 MiB (6bbdbf77, 2fdb83a5; cloud/test/relay.test.ts sends one message at exactly the cap and failed with the old budget). A real 4.99 MB icon over the relay now saves in 1.9 s; cloud tests pass 133/133. The relay change takes effect only once the cloud is deployed.
  • P2 account-computers.ts: no PostHog events. The desktop app now reports the phones' access_requested and access_request_failed (denied/expired/cancelled). The host's track accepts both, and they're listed in docs/reference/analytics-events.md. Approval stays the host's device_paired. A cancel that lands while a status poll is in flight counts once. renderer/lib/account-computers.test.ts covers each event, plus that case, which failed before the fix.
  • P2 account-channel.test.ts: e2e. See below.

Checks:

  • cd host && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test: passed (791 tests, 0 failed, 1 ignored).
  • cd apps/desktop && npm ci && npm run typecheck && npm test: passed (406 tests, 0 failed).
  • In this worktree's sandbox (npm run dev -- --cdp, its own host and data, dev cloud off): the e2e smoke spec passed. A throwaway probe showed the real preload streaming host events before and after a page reload (20 lines each).

Not run: the two-computer flow (ask for access, approve, chat, reconnect, revoke). It needs a dev account signed in on two computers with the dev cloud, and my sandbox has no signed-in account. For the same reason, agent-logos and call-conversation stopped at the sign-in screen. I didn't use my installed app or its data instead.

# Conflicts:
#	apps/desktop/src/renderer/client/host-client.ts
- Each call of an access request takes a current session token, so a
  computer that answers after the first token expires can still finish
  the exchange. The request stays with the account that made it, and a
  switch of account ends it.
- Cancel before the computer shows its code now withdraws the request
  the main process is waiting on, instead of leaving it polling until
  it expires.
- A withdrawal that fails is logged; the request then leaves the
  computer's screen when it expires.
Electron's crypto (BoringSSL) has no ChaCha20-Poly1305, so every relay
channel failed with "Unknown cipher" in the running app while the unit
tests, which run in Node, passed. The frames now use @noble/ciphers and
still match the shared relay vectors byte for byte.
When a computer revokes this app, its bots read "No access" but stayed
in the sidebar, and Settings kept offering Open until a manual refresh.
The account is now listed again as soon as the computer refuses, so its
bots go and it reads Ask for access, as on the phones.
@plotarmordev

Copy link
Copy Markdown
Author

Updates since the last review:

  • I merged dev in (one conflict in host-client.ts: the 401 message now goes through trNow, as on dev).
  • I ran the two-computer flow for real: two hosts in one test account on a local cloud, with the desktop app signed in. Its first run found that no account computer opened in the real app. Electron's crypto has no ChaCha20-Poly1305, so every channel failed with "Unknown cipher", while the unit tests, which run in Node, passed. Fixed with @noble/ciphers (a2252810). The frames still match the shared relay vectors.
  • The same run showed a revoked computer staying in the sidebar until a manual refresh. It now drops out on its own and offers Ask for access (373a9105).
  • Three of Greptile's findings on this code were posted on End-to-end tests run omp with a throwaway home, so no test sessions are left in the user's account #340 instead of here. They're fixed in 35097aff: each access-request call takes a current token; Cancel before the code shows withdraws the request; a failed withdrawal is logged.
  • All 8 flows now pass, and npm run e2e passes 5/5 signed in. Details and the one check I couldn't run (SSH takeover) are in the description's Validation section.

The relay's per-link budget of 8 MiB per 10 s counted wire bytes, while the
host's 8 MiB cap for an approved device counts plaintext. Sealed into
base64url frames, a 5 MiB icon is about 8.9 MiB and a message at the cap
about 10.7 MiB, so the relay closed the link with 4008 rateLimited before the
host ever saw it. The budget is now 12 MiB.
The JSON wrapper around the icon data is 17 bytes, so the message was 8 MiB + 1.
…open-upstream-pr-desktop-app-opens-bots-

# Conflicts:
#	apps/desktop/src/renderer/store/cloud.ts
#	apps/desktop/src/shared/analytics.ts
#	host/src/analytics/reported.rs
- Pace outgoing frames to the relay's per-link budget (200 messages,
  12 MiB per 10 s), so a large attachment no longer trips 4008 and
  drops the channel mid-upload.
- Read the relay's upgrade status (the `ws` client exposes it; Node's
  built-in WebSocket doesn't): a fourth 403 in a row ends calls with
  401, so a revoked device shows No access instead of retrying forever.
- Close a link that heard nothing back between two pings, so the next
  call opens a fresh socket instead of reusing a deaf one.
@plotarmordev

Copy link
Copy Markdown
Author

Thanks for the review. I fixed all three findings in ad35909:

  • Large uploads drop the connection: outgoing frames now pass through main/relay-pacer.ts, which keeps each link within the relay's budget (≤ 200 messages and ≤ 12 MiB per window, with a 1 s margin) across all calls, in order. A large attachment waits out the window and no longer gets closed with 4008.
  • Removed access keeps retrying: the channel now uses ws, because the Node WebSocket built into Electron 44 reports a refused upgrade with no status, and I checked that in Electron. unexpected-response gives the relay's status. Three 403s in a row stay retryable, since the relay can briefly lag right after an approval, which matches the phones. The fourth ends calls with 401, so the store reads No access and lists the account again.
  • Silent sockets block reconnecting: the relay answers every ping, so a link that heard nothing between two 30 s pings fails as unreachable. Calls and subscriptions end, and the next call opens a fresh socket.

There are new unit tests for each in src/shared/relay-pacer.test.ts and src/shared/account-channel.test.ts. npm run typecheck and npm test pass.

…open-upstream-pr-desktop-app-opens-bots-

# Conflicts:
#	apps/desktop/src/renderer/views/settings/ComputersView.tsx
@Blankeos

Blankeos commented Nov 7, 2026

Copy link
Copy Markdown

I need this so bad 🫠

Upstream added its own relay channel (main/channel/) and cloud access
request for Codync Cloud computers. This branch now builds on those:
the duplicate account-channel/channel-crypto modules are gone, the
relay pacer moves into main/channel/, the access request gains the
6-digit code and Cancel for computers that aren't cloud-hosted, and
useCloudAttachments opens every approved account computer.
Comment thread apps/desktop/src/preload/index.ts Resolved
- A computer that rejoins while frames still wait on the relay's rate
  window no longer gets those frames, sealed for the old channel, ahead
  of the new handshake; what already left still counts against the
  window.
- An access code that arrives after its request was withdrawn no
  longer shows on, or counts for, the next request to that computer:
  each request carries its own id.
@plotarmordev

Copy link
Copy Markdown
Author

Thanks for both findings, fixed in 430de71.

  • Old frames break reconnecting: when the channel drops (presence), RelayChannel.drop() now clears what the pacer still holds, so the new hello goes first. RelayPacer.clear() (was stop()) keeps the record of what already left, so the socket's rate budget stays intact. Tests: relay-channel.test.ts (rejoin mid-window: no old frame goes out, and the channel gets back to ready) and relay-pacer.test.ts.
  • New request shows old code: the preload gives each requestAccess its own id, cloud:accessCode carries that id instead of the computer's, and the store also ignores a code that arrives after Cancel. Tests: preload-streams.test.ts and account-computers.test.ts.

I ran npm run typecheck and npm test (482 pass) in apps/desktop.

…open-upstream-pr-desktop-app-opens-bots-

# Conflicts:
#	host/src/analytics.rs
…open-upstream-pr-desktop-app-opens-bots-
- A computer that turns this device away now refreshes the account's
  computers right away, so its bots go and Settings offers to ask
  again: the attachments watch each attached store's connection, which
  reaches neither the app nor the account's list.
- Settings keeps a computer opened through the relay under "Other
  computers in your account" with Open, instead of hiding it once its
  store exists; only this computer and SSH ones have their own card.
@plotarmordev

Copy link
Copy Markdown
Author

Thanks for both findings; fixed in 3785088:

  • Refused access: I now watch each attached store's own connection in useCloudAttachments (released when it detaches), so an unauthorized or computerOffline store refreshes the account's computers right away: its bots go and Settings offers Ask for access again.
  • Settings: others now hides only computers this app manages (isLoopback: this one and SSH ones), so a computer opened through the relay keeps its row with Open.

I also merged the latest dev and bumped to 2.45.0, since 2.44.1 is tagged. Desktop typecheck and npm test (493) pass, and so do host fmt, clippy and cargo test.

A call queued behind a full relay window was sealed at once, so its
timeout could fire before it left; the queued cancel then followed it
and the computer still ran the change. The pacer now takes a function
and makes each message only once the window has room for it: a call
gets its id and frames when it leaves, and one given up on while it
waited is never sent. The handshake's deadline likewise starts when
hello leaves, not when it is queued.
@plotarmordev

Copy link
Copy Markdown
Author

Thanks, both fixed in 578cd7d. The pacer now takes a function and makes each message only once the relay's window has room for it. A call gets its id and is sealed when it leaves, so if it times out while it waits it is never sent (and no cancel goes out for it). The handshake's 10 s deadline now starts when hello is written, not when it is queued. I added regression tests to relay-channel.test.ts: a call that times out behind a full window never reaches the computer, and a rejoin within the window keeps its socket and then sends the calls that never left. Both fail on the previous head.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request P3: low Nice to have: features, questions, polish.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants