From 4f84969e9309439892eae0f60930dd604265f799 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:40:08 -0700 Subject: [PATCH] docs: add remote server update advisor design doc Co-authored-by: Orca --- .../reference/remote-server-update-advisor.md | 486 ++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 docs/reference/remote-server-update-advisor.md diff --git a/docs/reference/remote-server-update-advisor.md b/docs/reference/remote-server-update-advisor.md new file mode 100644 index 00000000000..496f8819d33 --- /dev/null +++ b/docs/reference/remote-server-update-advisor.md @@ -0,0 +1,486 @@ +# Remote Server Update Advisor + +## Decision + +Build a remote server update advisor before building any remote self-update +executor. + +When the protocol compatibility gate blocks a paired Orca server, Orca should +tell the user what is wrong, identify the server version and likely install +shape when it can, and provide copyable platform-specific update guidance. The +client must not download, install, restart, or run privileged commands on the +remote machine in this design — not even over an SSH connection Orca already +holds. + +The advisor covers **both block directions** reported by +`evaluateRuntimeCompat` (`src/shared/protocol-compat.ts`): + +- `server-too-old`: show the remote update guidance described in this doc. +- `client-too-old`: do not show server guidance. Route to the local update + flow — the desktop updater (`src/main/updater.ts`) on desktop, the app-store + update message on mobile. + +The first implementation adds optional server-side metadata now, but treats it +as advisory and untrusted: + +```ts +type RuntimeUpdateInfo = { + currentVersion?: string | null + latestVersion?: string | null + updateAvailable?: boolean | null + installKind?: RuntimeInstallKind + restartKind?: RuntimeRestartKind + serviceName?: string | null + installPath?: string | null + hostArch?: string | null // process.arch, e.g. 'x64' | 'arm64' + docsUrl?: string +} + +type RuntimeInstallKind = + | 'mac-app' + | 'mac-homebrew' + | 'windows-installer' + | 'linux-appimage' + | 'linux-deb' + | 'linux-rpm' + | 'source' + | 'unknown' + +type RuntimeRestartKind = 'desktop' | 'foreground-serve' | 'systemd' | 'unknown' +``` + +The protocol compatibility gate remains the sole source of truth for whether a +client can use the server. `latestVersion` and `updateAvailable` improve the +prompt, but the app must render useful guidance when every field is absent. + +## Problem + +Remote Orca servers can be desktop apps, headless Linux AppImages, systemd +services, Windows machines, deb/rpm package installs, or source builds. A +client that only says "update the server" leaves users stranded at the exact +moment the app is blocked. A client that attempts to update the server +automatically risks running the wrong package manager, requiring `sudo`, +breaking a service, or mutating a machine the user did not intend to touch. + +The right first step is to make the blocked state actionable without taking +control of the remote host. + +### The cold-start constraint + +`updateInfo` only exists on servers new enough to ship it — and the servers +being blocked are, by definition, old. The first generation of blocked servers +will send **no** `updateInfo` at all. The advisor must therefore be fully +functional from client-side data alone: the compat verdict, the already-shipped +`hostPlatform` field on `RuntimeStatus` (`src/shared/runtime-types.ts` — itself +optional; servers older than it land in the unknown-install guide), the +paired endpoint's port, and client-side release metadata. `updateInfo` is an +investment that pays off the *next* time a server falls behind. This also +means steps 1–2 of the rollout should reach a release as early as possible, +independent of the UI. + +## Goals + +- Show a clear "Update server required" prompt when protocol compatibility + blocks a paired server, reusing the existing entry points + (`HostSectionHeaderMenu.tsx`, `RuntimeEnvironmentsPane.tsx`). +- Handle `client-too-old` by routing to the local update flow instead. +- Show running and required protocol versions from the compat verdict. +- Add optional update metadata to runtime status so future blocked states can + display server version, latest known version, install kind, and restart kind. +- Provide copyable update commands or manual steps for the detected + platform/arch/install kind, and a useful fallback when unknown. +- Support the SSH use case: the guidance is written to be pasted into the + user's own SSH session on the server. Orca does not run it. +- Never block the compat gate or the status path on a network call. + +## Non-Goals + +- Do not run update commands on the remote server, via any transport — + including SSH connections Orca already holds for SSH workspaces. +- Do not invoke `sudo`, package managers, installers, or service restarts. +- Do not silently replace AppImages or desktop apps. +- Do not require exact app-version equality between client and server + (protocol versions decide compatibility; app versions are display/advice). +- Do not build a new stateful update service for the MVP. +- Do not cover the SSH relay (`src/main/ssh/ssh-relay-deploy.ts`). The relay + is client-deployed into content-hash-versioned install dirs and self-heals on + version mismatch; it never needs this advisor. This doc is about paired + `orca serve` runtime servers. + +## Design + +### Trust model + +Every `updateInfo` field crosses a trust boundary: it is produced by a remote, +possibly compromised, server and some of it is rendered into commands the user +is invited to paste into a root shell. Rules: + +- Command blocks are rendered **only from client-owned templates**. Server + data may select a template or fill a validated placeholder; it is never + concatenated into a command as free text. +- `serviceName`: must match `^orca[A-Za-z0-9@:._-]{0,75}\.service$` — the + mandatory `orca` prefix mirrors the trust rule cgroup-based systemd + detection already applies ("trusted only when the unit name starts with + `orca`"), so a compromised server cannot aim the rendered + `sudo systemctl restart` at an unrelated unit such as `sshd.service`, and + the leading alphanumeric means the value can never parse as a `systemctl` + flag. Otherwise it is discarded and the default `orca-serve.service` is + used. +- `installPath`: must be absolute, end in `.AppImage`, and match + `^[A-Za-z0-9/._~+-]{1,256}$` (no spaces, quotes, or shell metacharacters) or + it is discarded and the documented default `/opt/orca/orca-linux.AppImage` + is used. The `.AppImage` suffix is load-bearing: the template `sudo mv`s + onto this path, so without it a malicious server could aim the overwrite at + an arbitrary root-owned file. Only the Linux AppImage template renders + ``; typical Windows paths (drive letters, backslashes) fail + the regex anyway, and the Windows and macOS guides never embed a path in a + command. +- `currentVersion` / `latestVersion`: must match + `^\d+\.\d+\.\d+(-[A-Za-z0-9.-]{1,40})?$` or they are not displayed. +- `docsUrl`: parsed with `new URL()` and rendered only if the origin is + `https://github.com` and the **parsed** pathname is `/stablyai/orca` or + starts with `/stablyai/orca/`; otherwise the client's built-in links are + used. Checking the parsed pathname defeats `…/orca/../other-repo`, which a + raw string-prefix check passes and browsers normalize to another repo; the + trailing slash rules out sibling names like `stablyai/orca-foo`. +- Port: taken from the client's own paired endpoint (an integer the client + already validated), never from server metadata. Behind a tunnel or proxy it + can differ from the server's listen port, so guides treat it as a hint. +- Enum fields (`installKind`, `restartKind`): unrecognized values are treated + as `unknown`. `hostArch` is a free-form `process.arch` string: anything + other than a recognized arch (`x64`, `arm64`) is treated as absent. + +Values that fail validation are treated as absent — no error, no partial +rendering. + +### Runtime status metadata + +Add optional `updateInfo` to `RuntimeStatus`, alongside the existing +`runtimeProtocolVersion` / `capabilities` / `hostPlatform` fields, emitted from +the same place status is stamped today (`src/main/runtime/orca-runtime.ts`). +Older servers omit it; all client handling must be defensive. + +Detection is computed **once at server startup and cached** — never on the +status hot path, which is polled. No package-manager subprocess may run per +status request. Signals, in order of reliability: + +- `source`: `app.isPackaged` is false. +- `linux-appimage`: `process.env.APPIMAGE` is set (the AppImage runtime sets + it); its value is `installPath`. +- `windows-installer`: win32 and `process.execPath` under the NSIS install + location. +- `mac-app`: darwin and `process.execPath` inside a `.app` bundle. Homebrew + cask installs copy the app into `/Applications`, so `mac-homebrew` is not + reliably distinguishable at runtime; when ambiguous, report `mac-app` (the + mac guide includes a Homebrew alternative). `mac-homebrew` stays in the enum + for explicit future configuration. +- `linux-deb` / `linux-rpm`: non-AppImage packaged Linux install (execPath + under the package install dir); the deb-vs-rpm distinction may use a single + package-ownership check at startup. If the check is skipped or fails, report + `unknown`. +- `restartKind: systemd`: `/proc/self/cgroup` names a `.service` unit under + `/system.slice/` — which also yields `serviceName`. A user-slice `.service` + cgroup is trusted only when the unit name starts with `orca`: children share + their parent's cgroup, so a manual `orca serve` inside a terminal or + multiplexer that itself runs as a user service (e.g. + `gnome-terminal-server.service`, a tmux user unit) would otherwise + false-positive. `INVOCATION_ID` alone is not trusted for the same + inheritance reason. +- `restartKind: desktop` when serving from the full desktop app; + `foreground-serve` for `orca serve` with no systemd markers. + +Detection must prefer `unknown` over guessing, and prefer the broader kind +over an unverified narrower one. + +`hostArch` is `process.arch`, needed because Linux assets differ by arch +(`orca-linux.AppImage` vs `orca-linux-arm64.AppImage`). When absent (old +server), the guide shows the x64 command with an arm64 note rather than +guessing. + +### Client prompt surfaces + +Use the same advisor content everywhere a blocked remote server surfaces: + +- Runtime environment settings row (`RuntimeEnvironmentsPane.tsx`): the + strongest detail view — versions, detected install/restart kind, copyable + commands, docs links, and "Check again" (re-fetches status and re-evaluates + compatibility). "Check again" disables with a spinner while the recheck is + in flight; on success the blocked state clears back to the normal server + view, and on continued incompatibility an inline note confirms the server + is still out of date — never a click with no visible response. +- Sidebar host menu (`HostSectionHeaderMenu.tsx`): keep the existing + "Update server required" action; deep-link to the settings row. +- Blocked action dialog or toast: the compat gate already throws a + `RUNTIME_COMPAT_BLOCK_CODE`-tagged error + (`src/renderer/src/runtime/runtime-protocol-compat.ts`), so surfaces can + distinguish a version block from a transport failure and offer the same + deep-link. + +The advisor needs the blocked server's `RuntimeStatus` (`hostPlatform`, +`updateInfo`) and the compat verdict even though the gate throws. Today the +`RUNTIME_COMPAT_BLOCK_CODE` error carries only a message and code, discarding +both: attach the verdict and `RuntimeStatus` to that error (e.g. +`error.verdict`, `error.status`), and have "Check again" read status through +the raw non-asserting RPC (`status.get` + `unwrapRuntimeRpcResult`) rather +than `getRuntimeEnvironmentStatus`, so detection and versions can render +while the environment stays blocked. + +Primary copy for `server-too-old`: + +> This Orca server needs an update before this client can use it. + +For `client-too-old`, keep the existing `describeRuntimeCompatBlock` message +and offer the local updater action instead of any server guidance. + +When detection is available, include a line such as: + +> Detected: Linux AppImage, systemd service. + +The line comma-joins only the fields that resolved — e.g. "Detected: Linux +AppImage" when `restartKind` is `unknown` — and is omitted entirely when both +are unknown. + +Command blocks are copyable one block at a time. A command block is a +suggestion for the user to run on the server (typically over their own SSH +session), never a command Orca executes. + +The mobile client duplicates the compat evaluators (see the header comment in +`protocol-compat.ts`); the guide matrix and copy should live in shared, +dependency-free modules so mobile can mirror them. + +### Update guide matrix + +The client owns a guide matrix keyed by `hostPlatform`, `hostArch`, +`installKind`, and `restartKind`. It ships with the client and needs no server +deploy to change. Placeholders below (``, ``, +``) are filled from validated metadata or documented defaults +(`/opt/orca/orca-linux.AppImage`, `orca-serve.service`, `6768` — matching +`docs/reference/headless-linux-server.md`). + +All Linux download commands write to a sibling temp file and `mv` into place. +Overwriting a running binary in place fails with `ETXTBSY`; `mv` within the +same directory is an atomic rename that works while the old version runs. Do +not stage in `/tmp` — a cross-filesystem `mv` degrades to a non-atomic copy +into the destination path, which reintroduces the same failure. + +Linux AppImage (x64; use `orca-linux-arm64.AppImage` on arm64), download and +swap: + +```bash +sudo curl -fL https://github.com/stablyai/orca/releases/latest/download/orca-linux.AppImage \ + -o .new +sudo chmod +x .new +sudo mv .new +``` + +Then restart: + +- systemd service: `sudo systemctl restart ` +- foreground server: stop the running `orca serve` (Ctrl+C) and re-run your + usual serve command, e.g. + `LIBGL_ALWAYS_SOFTWARE=1 serve --port ` plus any flags + you normally pass (such as `--pairing-address`). The advisor cannot know the + original command line, so it must not present an invented one as complete. + +Debian or Ubuntu package (assets are versioned: `orca-ide__amd64.deb`, +or `_arm64.deb` on arm64; when release metadata supplies the exact asset URL +the advisor prepends a matching `curl -fLO ` line — here and for +rpm — otherwise it links the releases page and the command uses the downloaded +filename): + +```bash +curl -fLO # included when release metadata supplies the exact asset URL +sudo apt install ./orca-ide__amd64.deb +sudo systemctl restart # if running as a service +``` + +Fedora, RHEL, or compatible RPM package (`orca-ide-.x86_64.rpm` / +`.aarch64.rpm`): + +```bash +curl -fLO # included when release metadata supplies the exact asset URL +sudo dnf install ./orca-ide-.x86_64.rpm +sudo systemctl restart # if running as a service +``` + +macOS desktop app: + +- Open Orca on that Mac and use the in-app update flow, or download the latest + `orca-macos-.dmg` from the releases page and replace the app. +- If it was installed with Homebrew: `brew update && brew upgrade --cask + stablyai/orca/orca --greedy` (the cask is marked `auto_updates`, so plain + `brew upgrade` skips it; `--greedy` is required). +- Restart the paired server after the app updates. + +Windows installer: + +- Open Orca on that Windows machine and use the in-app update flow, or + download and run + `https://github.com/stablyai/orca/releases/latest/download/orca-windows-setup.exe`. +- Restart Orca (or the `orca serve` process) after installation. + +Source build: pull and rebuild; link the repo README. + +Unknown install: + +- Show the server platform and version if known. +- Link `docs/reference/headless-linux-server.md` (on GitHub) and the releases + page. +- Tell the user to update Orca on the server machine, restart the server, and + click "Check again". + +Exact deb/rpm filenames are versioned and must come from release metadata. If +the client does not know the exact asset URL, it shows manual download +guidance — it never invents a versioned filename. The unversioned +`releases/latest/download/` URLs are stable only for the artifacts with +unversioned names in `config/electron-builder.config.cjs`: +`orca-linux.AppImage`, `orca-linux-arm64.AppImage`, `orca-macos-.dmg`, +and `orca-windows-setup.exe`. + +### Release metadata + +No new update service. The desktop updater already publishes electron-updater +manifests at a stable generic feed +(`https://github.com/stablyai/orca/releases/latest/download` — see +`src/main/updater.ts`): `latest-linux.yml`, `latest-mac.yml`, and `latest.yml` +carry the latest version and asset filenames (the arm64 Linux release +publishes its own `latest-linux-arm64.yml` variant). The advisor picks the +manifest matching the *server's* platform/arch — not the client's — and reuses +it for `latestVersion` / `updateAvailable` / exact asset names. + +Constraints, all inherited from known updater behavior: + +- The fetch is best-effort with a short timeout, done lazily when the advisor + is shown — never on the compat gate or status path. Failure means + `updateAvailable: null` and version-less guidance, not an error. +- During release transitions, GitHub can expose a release before its assets + are reachable (see the walk-back logic in `updater.ts`). A fetched manifest + that names unreachable assets must degrade to the manual releases-page link, + not a broken copyable URL. +- Results may be cached briefly on the client; staleness only affects advice + text, never compatibility. +- If a server ever populates `latestVersion` / `updateAvailable` itself (e.g. + from its own updater check at startup), the client-fetched manifest wins: + server values are startup-stale hints and are never refreshed on the status + path. + +A stateful update service is only needed later for staged rollouts, kill +switches, signed org policy, private channels, or fleet controls. + +## Rollout + +Ordered so each piece is independently safe; one PR works, but steps 1–2 may +land earlier on their own (see the cold-start constraint): + +1. `RuntimeUpdateInfo` type in `src/shared/runtime-types.ts`; startup-cached + detection in main; emit `updateInfo` in runtime status + (`src/main/runtime/orca-runtime.ts`). +2. Validation module for server-supplied fields (shared, dependency-free, unit + tested against injection inputs). +3. Client-side guide matrix keyed by platform/arch/installKind/restartKind + with the fallback chain above (shared module for mobile reuse). +4. Advisor UI in `RuntimeEnvironmentsPane.tsx` with both verdict directions: + `server-too-old` → guidance; `client-too-old` → local updater action. + "Check again" re-fetches status. +5. Deep-links from `HostSectionHeaderMenu.tsx` ("Update server required") and + from `RUNTIME_COMPAT_BLOCK_CODE` error surfaces. +6. Lazy release-metadata lookup (`latest-*.yml`) for `latestVersion`, + `updateAvailable`, and exact deb/rpm asset names. + +## Testing + +- Unit: detection mapping per platform fixture (APPIMAGE env, systemd cgroup + line — including a user-slice terminal-service cgroup that must not map to + `systemd`, `app.isPackaged`); every validation regex rejects + shell-metacharacter payloads (`; curl … | sh`, backticks, `$()`, newlines) and accepts the + documented defaults. +- Unit: guide matrix snapshot per (platform, arch, installKind, restartKind) + key plus the all-fields-absent cold-start case — the doc's own commands are + the expected output. +- Unit: verdict branching — `client-too-old` never renders server commands. +- Extend `runtime-compatibility-test-fixture.ts` (today it only builds + compatible-status responses) with blocked-status fixtures with and without + `updateInfo`. +- Manifest fetch: timeout and 404 paths yield `updateAvailable: null` and the + fallback guide; no path throws into the compat gate. + +## Alternatives Considered + +### Full remote self-update + +The client could download an asset on the remote machine, install it, and +restart the server — including over an SSH connection Orca already holds for +SSH workspaces. + +Rejected for the first version. Remote hosts vary too much: AppImages, systemd +services, desktop apps, Windows installers, package managers, and source +builds all have different permission and restart models. Running install +commands remotely — even over Orca-owned SSH — creates a trust and audit +problem, especially when `sudo` or service restarts are involved. It can be +revisited as an explicit, capability-gated flow that asks for confirmation and +shows exactly what will run. + +### Protocol-only error message + +Keep the current compatibility block and only say the server is too old. + +Rejected. Technically safe but not user-safe: the user still has to infer +which machine is stale, how Orca was installed there, which asset to download, +and whether a service restart is needed. + +### Client-only latest-version check + +Compare the remote protocol against the client's own app version or update +feed, with no server metadata. + +Rejected as the only mechanism. The client's platform and install channel need +not match the server's — a macOS client may pair with a Linux AppImage server. +Client-side release data feeds the guides, but cannot describe the server's +install shape. + +### Require a new Orca update server + +Rejected for the MVP. The advisor needs best-effort latest-version and asset +metadata, which the existing electron-updater manifests already provide. + +### Exact version lockstep + +Require the server to run the exact client app version. + +Rejected. The protocol contract in `protocol-compat.ts` already defines +compatibility, and exact equality would fail compatible pairs. App versions +are for display and advice only. + +## Deferred / Open Questions + +### From 2026-07-06 review + +- **Blocked surface left undecided between dialog and toast** — Client prompt surfaces (P1, design-lens, confidence 75) + + Dialog and toast are not interchangeable: a dialog is modal, traps focus, + and requires explicit dismissal, while a toast is transient, non-blocking, + and typically announced as a polite live region. The doc says "Blocked + action dialog or toast" without choosing, leaving the implementer to pick + focus-management and dismissal behavior on their own, risking inconsistent + UX if this surface and future blocked-action surfaces land differently. + +- **Advisor assumes the blocked user can run sudo on the server** — Goals / Client prompt surfaces (P2, product-lens, adversarial, confidence 75) + + Every actionable path the advisor produces is a sudo command pasted into + the user's own SSH session, which assumes the blocked user is also the + server's administrator. On shared or team-administered servers the + copyable commands are un-runnable and the block stays exactly as + unactionable as the plain "update the server" message. A copyable "send + these steps to whoever administers this server" handoff would keep the + goal met for non-admin users. + +- **Server-metadata pipeline is sequenced first but delivers no value to currently-blocked servers** — Rollout / cold-start constraint (P2, product-lens, adversarial, confidence 75) + + By the doc's own logic, rollout steps 1–2 (updateInfo schema, cached + detection, trust model) help no server blocked today — they pay off only + the next time a server falls behind, and that window's population is + unvalidated. The client-only advisor (steps 3–5) already delivers the full + user-facing unblock; consider shipping it as v1 and treating steps 1–2 and + 6 as a fast-follow once advisor engagement is confirmed, or naming the + expected frequency of the payoff window to justify front-loading a + permanent wire contract.