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.
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:
- 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 intoTools/<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); - downloads the platform archive (
agentbridge-<rid>.tar.gz) to%TEMP%; - extracts it to a temp folder;
- spawns the new executable from the temp extract as an updater process
(
--apply-update <target> <extract> <oldPid>) and exits; - the updater waits for the old process to terminate, copies the changed files
(the executable last, via a
.oldrename 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.
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 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).
- Current version:
Assembly.GetExecutingAssembly().GetName().Version— the numeric1.yy.MM.ddbaked into the binary by the csproj (the-prereleasesuffix lives only in the informational version, so a prerelease build compares by its numeric date). - Latest version: an HTTP
HEAD/GETtohttps://github.com/Graphene-Lab/AgentBridge/releases/latestwithAllowAutoRedirect = false; the redirect'sLocationheader 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.
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.
/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), notdotnet— the check refuses under the dotnet host, because the swap would targetdotnetitself. Release installs run from the apphost: launchagent.exe(Windows) /agent(Linux/macOS); - GitHub unreachable / agents busy / another instance is running (see below).
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./updatethen refuses with "another AgentBridge instance is running" — run/updatefrom 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.
- 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 assetup.json— updates never touch it). appsettings.json:"AutoUpdate": { "Enabled": true }is the shipped default.- Command line:
--no-updatedisables 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.
- The updater restarts the app with the original command line (minus
--no-update), so a TUI session stays a TUI and a--headlessservice stays headless. - The old executable is kept as
agent(.exe).olduntil the next successful start, which deletes it (and any stale%TEMP%\agentbridge-updatearea). A broken new executable can be recovered by renaming the.oldback manually. - If the old process does not exit within two minutes, the updater aborts and the old version keeps running.
- Downloads travel over HTTPS from GitHub's CDN; archives are not checksum-verified
(GitHub does not publish per-asset hashes). A
sha256.txtasset could be added torelease.ymllater 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.
- 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.Enabledto 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.