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:
- 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.
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) |
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, with two exceptions |
The exceptions: appsettings.json (server config) and providers.json (LLM
provider definitions) are stripped from the extracted copy before the swap, so the
user's configuration is never overwritten. Everything else β including the other
.json files in the archive (.playwright/package/*.json,
agent.staticwebassets.endpoints.json) β is distribution content and is replaced.
Protection is by whitelist, 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 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.
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 entry assembly is a .dll, so the swap would target dotnet itself.
Enabling / disabling
- Default: enabled (
true). - TUI: menu File β 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.
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--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.
Security notes
- 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.
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.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.