Skip to content

Latest commit

 

History

History
171 lines (136 loc) · 9.57 KB

File metadata and controls

171 lines (136 loc) · 9.57 KB

AgentBridge — automatic updates (architecture)

How AgentBridge updates itself from the GitHub Releases page, and why it is designed this way. Implemented in AutoUpdate.cs (a self-contained static class) with two hooks in Program.cs and one menu item in Tui.cs.

What it does

At startup the app asks GitHub for the latest release. When the released version is newer than the running one — and auto-update is enabled — it:

  1. refreshes the tool plugins first (PluginUpdater): every loaded plugin is checked against its repo's GitHub release and, when a newer self-contained zip exists, installed into Tools/<Plugin>/ before the app archive is applied (the archive also carries the plugins — this covers plugin releases that landed between two app releases). When agents are executing the refresh is refused and the whole update is retried after 30 minutes (ScheduleRetryIn);
  2. downloads the platform archive (agentbridge-<rid>.tar.gz) to %TEMP%;
  3. extracts it to a temp folder;
  4. spawns the new executable from the temp extract as an updater process (--apply-update <target> <extract> <oldPid>) and exits;
  5. the updater waits for the old process to terminate, copies the changed files (the executable last, via a .old rename for rollback), restarts the app with the original command line and cleans up.

Any failure along the way leaves the current version running untouched — the update is best-effort by design.

Why the two-process swap

On Windows a running executable cannot be overwritten, and at startup the app is already the process mapping its own exe — so the executable can never replace itself, neither during execution nor at the next start. The swap must be done by a different process: the new exe extracted to %TEMP% is a different file, so it can replace the installed one once the old process is gone.

This was verified both against Microsoft's documentation (CreateFile sharing modes: the image is opened read-only for the lifetime of the process) and empirically on Windows: a child process does not keep the parent's exe locked after the parent exits — the lock lasts only as long as the owning process. Hence the pattern "spawn the updater and exit immediately" (never wait for the updater from the app).

The file storage tiers (what an update may touch)

The update mechanism respects the three tiers defined in RELEASING.md:

Tier Rule
PersistentData\ (user-editable config: appsettings.json, providers.json, telegram.json + telegram.session, tools.json, rag_settings.json) never touched — not present in the archive, never written
OS app-data folder <AppData>\agent\ (SMTP/IMAP credentials, setup.json, autoupdate.json; LLM API keys are NOT here — they live per-provider in providers.json) never touched — outside the app folder by construction
Distribution content (everything else) replaced when changed — no exceptions, no whitelist

User-editable configuration lives ONLY under PersistentData\ (single-directory rule): the archive never contains it, so replacing the distribution content can never overwrite your settings. Legacy files left next to the executable by an older version are moved into PersistentData\ automatically on the first start. Every .json at the archive root (.playwright/package/*.json, agent.staticwebassets.endpoints.json) is generated content and is replaced like everything else — protection is by the directory rule, never by file extension.

Files are copied only when they changed (length + SHA-256 comparison); the executable is always replaced (never compared — a fresh build is the point of the update).

Version check (no GitHub API)

  • Current version: Assembly.GetExecutingAssembly().GetName().Version — the numeric 1.yy.MM.dd baked into the binary by the csproj (the -prerelease suffix lives only in the informational version, so a prerelease build compares by its numeric date).
  • Latest version: an HTTP HEAD/GET to https://github.com/Graphene-Lab/AgentBridge/releases/latest with AllowAutoRedirect = false; the redirect's Location header carries the tag (v1.26.8.10). No API call, so the unauthenticated rate limit (60 req/h/IP) is never an issue. A download URL is then constructed deterministically: https://github.com/Graphene-Lab/AgentBridge/releases/download/<tag>/agentbridge-<rid>.tar.gz.
  • GitHub imposes no bandwidth limit on release downloads (the Acceptable Use Policies "Excessive Bandwidth Use" clause is a relative anti-abuse rule, not a quota); the client still uses bounded timeouts so a slow/offline GitHub never blocks startup.

Platform detection

The archive name maps from the running OS + architecture (the RIDs built by release.yml):

Platform Archive
Windows x64 agentbridge-win-x64.tar.gz
Linux x64 / arm64 agentbridge-linux-x64.tar.gz / agentbridge-linux-arm64.tar.gz
macOS x64 / arm64 agentbridge-osx-x64.tar.gz / agentbridge-osx-arm64.tar.gz
anything else (e.g. Windows arm64) no archive — the check is skipped

The update is also skipped when the app runs from dotnet run/dotnet <dll> (dev mode): the check looks at the process executable — under the dotnet host the swap would target dotnet itself. Launching the published apphost (agent.exe / agent) is what enables updates; this holds for every apphost layout, single-file or not.

Manual update from the TUI (/update)

/update (menu Help → Check for updates… (/update)) forces the same GitHub check on demand and works even when the auto-update toggle is off. The TUI shows a short note for each outcome:

  • a newer release exists → the app downloads it (the status bar shows the download percentage — the archive is large), spawns the updater and closes itself; the updater swaps the files and the app comes back on the new version. If the app stays open, nothing was installed and the note says why:
    • already up to date — the running version equals the latest release;
    • no newer release yet — the running build is newer than the latest release (updates install GitHub releases, never local commits or unpushed work);
    • start the app with agent(.exe), not dotnet — the check refuses under the dotnet host, because the swap would target dotnet itself. Release installs run from the apphost: launch agent.exe (Windows) / agent (Linux/macOS);
    • GitHub unreachable / agents busy / another instance is running (see below).

Running as a service (systemd / launchd) or with auto-start

The two-process swap assumes a plain, user-launched process. When a supervisor owns the process the behavior adapts:

  • systemd / launchd: the update copies the changed files in place and exits — the service manager restarts the app with the new version (the SystemExtra unit uses Restart=always). The app never restarts itself there: its own restart would race the supervisor (duplicate instance, or the cgroup kill cutting the swap short).
  • Windows auto-start (Task Scheduler): when a second instance was launched manually while the auto-start instance is running the same agent.exe, the swap cannot replace the locked image. /update then refuses with "another AgentBridge instance is running" — run /update from that instance, or close it first (the auto-start one too), then retry.

Services that manage the binary themselves should still pass --no-update — they are the ones deciding when and how the files change.

Enabling / disabling

  • Default: enabled (true).
  • TUI: menu Help → Auto-Update toggles the check on/off and persists the choice to <AppData>\agent\autoupdate.json (the OS app-data folder, same tier as setup.json — updates never touch it).
  • appsettings.json: "AutoUpdate": { "Enabled": true } is the shipped default.
  • Command line: --no-update disables the check for that launch (for services/CI that manage the binary themselves — e.g. systemd units should pass it).

Precedence: --no-update > persisted toggle > appsettings.json > built-in true.

Restart and rollback

  • The updater restarts the app with the original command line (minus --no-update), so a TUI session stays a TUI and a --headless service stays headless.
  • The old executable is kept as agent(.exe).old until the next successful start, which deletes it (and any stale %TEMP%\agentbridge-update area). A broken new executable can be recovered by renaming the .old back manually.
  • If the old process does not exit within two minutes, the updater aborts and the old version keeps running.

Security notes

  • Downloads travel over HTTPS from GitHub's CDN; archives are not checksum-verified (GitHub does not publish per-asset hashes). A sha256.txt asset could be added to release.yml later if verification is wanted.
  • The binaries are unsigned; the trust model is "GitHub + HTTPS". Fine for personal distribution; sign the binaries before wider roll-out.

Testing

  • Unit-testable pieces: version comparison, RID mapping, "file changed" comparison (length + SHA-256).
  • End-to-end (Windows): run a published build from a folder, set AutoUpdate.Enabled to force a fake/lower current version (or run an old build), and observe the swap: temp extract → updater → .old → restart with the new exe.
  • The updater wait loop can be observed by keeping the old process busy past its exit deadline — the update must abort cleanly.