AgentBridge / docs /autoupdate.md
kocahoctpa's picture
AgentBridge showcase: docs, white paper, demo media and landing page
8f16a6b verified
|
Raw History Blame Contribute Delete
7.25 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:
0. **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`);
1. downloads the platform archive (`agentbridge-<rid>.tar.gz`) to `%TEMP%`;
2. extracts it to a temp folder;
3. spawns the **new** executable from the temp extract as an updater process
(`--apply-update <target> <extract> <oldPid>`) and exits;
4. 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](../docs-dev/RELEASING.md#what-an-update-must-never-touch--the-file-storage-tiers):
| 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 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 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 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.