Skip to content

feat(electron): in-app install, R2 auto-update feed, gaia:// deep links#2196

Merged
kovtcharov merged 3 commits into
mainfrom
feat/electron-install-update-plumbing-1721
Jul 18, 2026
Merged

feat(electron): in-app install, R2 auto-update feed, gaia:// deep links#2196
kovtcharov merged 3 commits into
mainfrom
feat/electron-install-update-plumbing-1721

Conversation

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Why this matters

The Agent UI shell had three broken/missing install-and-update surfaces, all in the Electron main/preload layer:

No silent fallbacks: a missing backend port, missing update feed, or malformed deep link each surface a loud, actionable error.

Part of #2014.

Dependency note (#1724)

The actual R2 publish feed + Worker latest*.yml channel pointer are milestone-52 work (#1719) and not live yet. This PR ships the app-side integration + config so it works against any configurable feed URL (a local/mock feed exercises the full path); electron-builder.yml switches provider: github → generic. Rollback (#1336) still lists/installs from GitHub Releases — independent of the forward feed, migrates with #1719.

Test plan

  • cd tests/electron && npm install && npx jest — full suite green (659 passed, 1 skipped, 20 suites)
  • New: tests/electron/deep-link.test.cjsgaia:// parse: valid, bad scheme, missing/invalid id, extra segments, argv extraction
  • New: tests/electron/auto-updater-feed.test.cjs — feed from env + config file, env precedence, loud NO_CHANNEL when unset, checkForUpdates picks up a later-configured feed
  • Updated: tests/electron/test_agent_process_manager.js — install/uninstall IPC redirect (POST/DELETE shape + X-Gaia-UI header), failed-install surfaces, loud no-backend error
  • Updated: tests/electron/auto-updater.test.cjsclearPin restores the R2 forward feed
  • cd src/gaia/apps/webui && npm run build — TypeScript + Vite build compiles
  • Manual: GAIA_UPDATE_FEED_URL=<mock> end-to-end update against a local feed (blocked on a real R2 feed / Publish Agent UI to R2 as an app (manifest + catalog entry + signed release workflow) #1719 for the production path)

…p links

Three Agent-UI shell surfaces that share the Electron main/preload layer.

#1721 — the agent:install / agent:uninstall IPC handlers were dead stubs that
threw "not yet implemented", so the UI could never import a published agent.
They now proxy to the backend install runtime (POST /api/agents/install, poll
install-status, DELETE /api/agents/{id}) — one tested install path — and stream
progress to the renderer over agent:install-progress. No backend port resolves
to a loud, actionable error rather than a silent failure.

#1724 — the forward auto-update channel moves off GitHub to a configurable
generic (R2) feed with a mutable "latest" channel pointer. The feed URL is
resolved at runtime from GAIA_UPDATE_FEED_URL or update-config.json; with none
configured the updater enters a loud no-channel state instead of silently
checking a dead feed. electron-builder.yml switches provider github -> generic.
The R2 publish feed + channel pointer are milestone-52 work (#1719); until then
point the env var at any generic/mock feed to exercise the path.

#1725 — register the gaia:// protocol and handle gaia://hub/install/<id> deep
links (macOS open-url, Win/Linux argv at cold start + second-instance),
dispatching the agent id to the install runtime. Malformed links surface an
actionable error dialog, never a silent drop.

Tests: deep-link parsing, install/uninstall IPC redirect + loud no-backend
error, and feed resolution / no-channel state under tests/electron.
@github-actions github-actions Bot added the tests Test changes label Jul 18, 2026
…ds pass

electron-builder aborts config load with "cannot expand pattern
${env.GAIA_UPDATE_FEED_URL}: env ... is not defined" when the var is unset,
which it is in the installer-build CI jobs. Replace the macro with a static
generic feed base under hub.amd-gaia.ai. The value is inert at runtime —
auto-updater.cjs always re-resolves the feed and calls setFeedURL, or enters
the loud no-channel state — it exists only so electron-builder can emit
app-update.yml at build time.
@kovtcharov

Copy link
Copy Markdown
Collaborator

🔒 SECURITY CONCERN — @kovtcharov-amd

The deep-link install path performs an agent install without any user confirmation: main.cjs dispatchDeepLink calls agentProcessManager.installAgent(command.agentId) directly, and the backend's trust enforcement only guards native packages. That means an external web page can trigger installation (download + pip install + hot-register) of any non-native catalog agent — including community/experimental tier — off a single generic OS "Open GAIA?" prompt, bypassing the trust gate that #2201 makes a hard requirement for the same action in-app. Recommend gating deep-link installs behind the same trust/confirmation dialog before this merges; happy to discuss specifics privately.

Separate note for the tracking issue rather than this PR: until code signing lands (#1719), auto-update integrity rests solely on HTTPS + same-origin checksum — fine as a documented accepted risk, but it should stay a hard blocker for enabling a production feed.

Otherwise the PR is in good shape — deep-link parser, feed resolution, and install/uninstall IPC proxy are all unit-tested (the main.cjs dispatch wiring is the untested piece, which is also where the concern above lives).

@kovtcharov-amd
kovtcharov-amd disabled auto-merge July 18, 2026 02:59
…onfirmation

A gaia://hub/install/<id> link comes from an untrusted web page, but dispatch
called installAgent() directly — so any non-native catalog agent could be
downloaded + pip-installed + hot-registered off a single generic OS "Open
GAIA?" prompt, with the trust gate only guarding native packages. That bypasses
the per-agent confirmation the in-app install requires (#2201).

Deep-link installs now require an explicit per-agent confirmation dialog (naming
the specific agent, defaulting to Cancel) before any download happens. The
dispatch flow moves into deep-link.cjs with confirm/installAgent/focusWindow
injected, so the security-critical gate — no confirm, no install — is unit
tested; main.cjs supplies the native confirmation dialog.
@github-actions

Copy link
Copy Markdown
Contributor

🟡 When the app starts in NO_CHANNEL (no feed configured) and a feed is later set, periodic background checks are never scheduled — only manual checkForUpdates() calls will work until the app restarts.

init() returns early at line 803 without reaching the initialCheckTimeout = setTimeout(...) block (line 807). scheduleNextCheck() is only ever called from inside that timeout callback, so the 4-hour background-check cadence is never initiated. checkForUpdates() itself re-resolves the feed on each call and will work, but nothing triggers it on a schedule after a late feed configuration.

The test "checkForUpdates() picks up a feed configured after init()" exercises a single manual check — it confirms the check runs but does not verify that periodic scheduling resumes. The gap is invisible to the test suite.

Since the R2 feed isn't live yet this won't affect users today, but it contradicts the intent of the late-configuration path the tests document.

🔍 Technical details

services/auto-updater.cjs — the new init() flow:

initialized = true;

if (!feedReady && !savedPin) {
    // …log…
    return;  // ← exits before the block below
}

initialCheckTimeout = setTimeout(async () => {
    await checkForUpdates();
    scheduleNextCheck();  // ← never reached when NO_CHANNEL at startup
}, CHECK_DELAY_MS);

Fix: when checkForUpdates() transitions out of NO_CHANNEL (i.e., _applyForwardFeed() returns true on line 497), kick off periodic scheduling if neither timer is already running:

if (!forwardFeedUrl && !state.pinnedVersion) {
    if (!_applyForwardFeed()) return;
    // Feed was just configured; start the periodic cadence if nothing else has.
    if (!initialCheckTimeout && !scheduledTimeout) scheduleNextCheck();
}

This mirrors the scheduleNextCheck() call that initialCheckTimeout would have made, without double-scheduling when a pin is active.

@kovtcharov kovtcharov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deep-link concern is fully addressed: installs from gaia:// now require an explicit per-agent confirmation (default Cancel), the gate lives in deep-link.cjs with injected deps, and the security-critical control flow is unit-tested — confirm-before-install ordering, decline, and dismissed-dialog (non-true) all covered. Note: the currently red build/Vitest checks are main's AgentHubGrid breakage (missing module after the #2190/#2201 merge ordering), not this PR — re-run after main is fixed and they should go green. The auto-update feed integrity note stands as an accepted risk tracked in #1719.

@kovtcharov
kovtcharov added this pull request to the merge queue Jul 18, 2026
Merged via the queue into main with commit 5bc7d32 Jul 18, 2026
50 of 57 checks passed
@kovtcharov
kovtcharov deleted the feat/electron-install-update-plumbing-1721 branch July 18, 2026 04:46
@itomek itomek mentioned this pull request Jul 22, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Test changes

Projects

None yet

2 participants