diff --git a/skills/browser-automation/desktop-remote-engine-sync/SKILL.md b/skills/browser-automation/desktop-remote-engine-sync/SKILL.md new file mode 100644 index 0000000..466b6a9 --- /dev/null +++ b/skills/browser-automation/desktop-remote-engine-sync/SKILL.md @@ -0,0 +1,175 @@ +--- +name: desktop-remote-engine-sync +description: Fix Desktop remote mode showing stale local engine version — sync engine version display and update to Bridge server instead of reading local install. +category: browser-automation +tags: [desktop, bridge, engine, remote-mode, version, update, fix] +related_skills: [atomk-desktop-development] +--- + +# Desktop Remote Mode — Engine Version Sync + +## Trigger + +- User reports "engine N commits behind" after installing new Desktop build +- Settings page shows stale version when Desktop is in remote/Bridge mode +- Engine update button doesn't affect the Bridge server's engine + +## Root Cause + +Desktop v4.0+ uses remote mode exclusively (atomlisting login → Bridge server). +The `get-hermes-version` and `run-hermes-update` IPC handlers fall through to +local `getHermesVersion()` / `runHermesUpdate()` — reading/writing +`~/.hermes/hermes-agent/` which is a stale leftover from a previous Desktop install. + +Compare `run-hermes-doctor` which correctly routes to Bridge in remote mode. + +## Fix Pattern (Two Repos) + +### 1. Bridge Server — New Endpoints + +Add to `cloud-bridge/server.py`: + +```python +# ── Hermes Engine Version & Update ────────────────────────── + +_HERMES_AGENT_DIR = Path(os.environ.get('HERMES_AGENT_DIR', + os.path.expanduser('~/hermes-agent'))) +_HERMES_BIN = str(_HERMES_AGENT_DIR / 'hermes') + + +async def hermes_version_handler(request: web.Request) -> web.Response: + """GET /api/hermes/version — runs hermes --version.""" + proc = await asyncio.create_subprocess_exec( + _HERMES_BIN, '--version', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=str(_HERMES_AGENT_DIR), + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15) + if proc.returncode != 0: + return web.json_response({'error': ...}, status=502) + return web.Response(text=stdout.decode().strip(), + content_type='text/plain; charset=utf-8') + + +async def hermes_update_handler(request: web.Request) -> web.Response: + """POST /api/hermes/update — runs hermes update.""" + proc = await asyncio.create_subprocess_exec( + _HERMES_BIN, 'update', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=str(_HERMES_AGENT_DIR), + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=120) + return web.json_response({'ok': proc.returncode == 0, 'output': ...}) +``` + +Register on both `ws_app` and `http_app` with `auth_wrapper`: + +```python +ws_app.router.add_get('/api/hermes/version', auth_wrapper(hermes_version_handler)) +ws_app.router.add_post('/api/hermes/update', auth_wrapper(hermes_update_handler)) +http_app.router.add_get('/api/hermes/version', auth_wrapper(hermes_version_handler)) +http_app.router.add_post('/api/hermes/update', auth_wrapper(hermes_update_handler)) +``` + +### 2. Desktop — Bridge Helper Functions + +Add to `src/main/index.ts`, near `runBridgeDoctor()`: + +```typescript +async function bridgeGetHermesVersion( + cloudBridgeUrl: string, apiKey: string, +): Promise { + const httpUrl = wsUrlToHttp(cloudBridgeUrl); + const resp = await fetch(`${httpUrl}/api/hermes/version`, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(15000), + }); + if (!resp.ok) return null; + return (await resp.text()).trim() || null; +} + +async function bridgeRunHermesUpdate( + cloudBridgeUrl: string, apiKey: string, +): Promise<{ success: boolean; error?: string }> { + const httpUrl = wsUrlToHttp(cloudBridgeUrl); + const resp = await fetch(`${httpUrl}/api/hermes/update`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(130000), + }); + const data = await resp.json(); + if (data.ok) return { success: true }; + return { success: false, error: data.error || ... }; +} +``` + +### 3. Desktop — Update IPC Handlers + +```typescript +// get-hermes-version +ipcMain.handle("get-hermes-version", async () => { + const conn = getConnectionConfig(); + if (conn.mode === "ssh" && conn.ssh) return sshGetHermesVersion(conn.ssh); + if (conn.mode === "remote" && conn.cloudBridgeUrl && conn.apiKey) // ← ADD + return bridgeGetHermesVersion(conn.cloudBridgeUrl, conn.apiKey); + return getHermesVersion(); +}); + +// refresh-hermes-version — same pattern + +// run-hermes-update +ipcMain.handle("run-hermes-update", async (event) => { + const conn = getConnectionConfig(); + if (conn.mode === "ssh" && conn.ssh) { ... } + if (conn.mode === "remote" && conn.cloudBridgeUrl && conn.apiKey) { // ← ADD + return bridgeRunHermesUpdate(conn.cloudBridgeUrl, conn.apiKey); + } + await runHermesUpdate(...); +}); +``` + +## Auth + +Desktop uses `conn.apiKey` from atomlisting login → sent as `Bearer` token. +Bridge endpoints use `auth_wrapper` (same as `/v1/*`, `/cdp/*`). +No separate API key provisioning needed. + +## Branch / PR Flow + +Both repos have protected `main`: +```bash +# Bridge +cd /home/ubuntu/atomk-page-bridge +git checkout -b release/vX.Y.Z +# ... edit server.py ... +git add cloud-bridge/server.py && git commit -m "feat: ..." +git push origin release/vX.Y.Z +# → Gitea API: create PR + merge + +# Desktop +cd /home/ubuntu/AtomK-Desktop +git checkout -b release/vX.Y.Z +# ... edit src/main/index.ts ... +git add src/main/index.ts && git commit -m "fix: ..." +git push origin release/vX.Y.Z +# → Gitea API: create PR + merge +``` + +## After Merge + +1. `git fetch origin main && git reset --hard origin/main` (both repos) +2. `sudo systemctl restart cloud-bridge.service` (Bridge changes take effect immediately) +3. Desktop needs rebuild for user to get the fix + +## Pitfalls + +- **`sudo` may not work**: use `systemctl restart` directly if running as root +- **Bridge restart can hang**: use `kill -9 ` then `systemctl start` +- **Gitea API 405 on merge**: sleep 3-5s after PR creation, retry +- **Protected main**: never push directly, always use release branch + PR +- **`_HERMES_BIN` path**: defaults to `~/hermes-agent/hermes`, overridable via `HERMES_AGENT_DIR` env +- **Version handler returns plain text** (not JSON) to match `hermes --version` format that Desktop's `parsedVersion` regex expects +- **hermes.ts unicode escapes**: do NOT use `patch` tool on hermes.ts — use Python `rb`/`wb` binary mode. See `atomk-desktop-development` pitfall #55. +- **Prompt hygiene**: `buildCloudBridgePrompt()` must never hardcode platform-specific URLs, menu structures, or account info. See `atomk-desktop-development` pitfall #59.