Skip to content

Remote-URL Updates ​

Clusto can fetch the daemon (clustod), the CLI (clusto), and the desktop / mobile app from an arbitrary HTTP(S) URL hosting a JSON manifest. This is independent of the desktop app's download-from-connected-daemon flow - it lets you stand up an update source behind any plain static HTTP server (S3, GitHub Pages, nginx, a raw folder served by python -m http.server for testing, etc.).

A daemon update is always a Clusto node update: it requires the matching cli artifact for the node's platform, verifies and stages both binaries, then installs or updates the CLI when the daemon restarts. Clusto does not expose a separate node CLI update control because the daemon and its local CLI are one release unit. If the staged CLI cannot be copied, made executable, or renamed into place on that restart, daemon startup fails instead of running a mixed daemon and CLI release.

During Clusto migration preparation, the legacy WebSocket UploadDaemon request is rejected even if its checksum is valid. It carries only a daemon and could strand a node with a mismatched CLI or without a verified A0 rollback image. Use a manifest URL through the app or clusto update-node --from ...; the manifest flow stages the daemon and CLI atomically.

Manifest format ​

A manifest is a single JSON document. Every entry carries a SHA-256 digest that is verified after download - the manifest itself is not signed, so the authenticity of the bytes ultimately rests on the transport (TLS) and the hash check.

json
{
  "version": "1.11.1",
  "build_date": "2026-05-12T11:00:00Z",
  "artifacts": [
    {
      "kind": "daemon",
      "platform": "linux-x64",
      "filename": "clustod",
      "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "size": 12345678,
      "url": "clustod",
      "build_date": "2026-05-12T11:00:00Z"
    },
    {
      "kind": "cli",
      "platform": "linux-x64",
      "filename": "clusto",
      "sha256": "abcd...",
      "size": 9876543,
      "url": "clusto"
    },
    {
      "kind": "app_installer",
      "platform": "windows-x64",
      "filename": "Clusto-win-x64-1.11.1-setup.exe",
      "sha256": "feed...",
      "size": 87654321,
      "url": "Clusto-win-x64-1.11.1-setup.exe"
    },
    {
      "kind": "android_apk",
      "platform": "android-arm64",
      "filename": "Clusto-android-universal-1.11.1.apk",
      "sha256": "cafe...",
      "size": 30000000,
      "url": "Clusto-android-universal-1.11.1.apk"
    }
  ]
}

Notes:

  • kind is one of daemon, cli, app_installer, android_apk.
  • platform slugs: windows-x64, linux-x64, linux-arm64, macos-x64, macos-arm64, android-arm64.
  • url is the location of the artifact bytes. Absolute URLs are used verbatim. Relative URLs are resolved against the manifest URL, so a folder layout like https://updates.example.com/clusto/{manifest.json,clustod,clusto,...} works out of the box.
  • build_date is optional. When present, it is written next to the artifact as a <filename>.builddate sidecar - the daemon's artifact picker uses that to prefer fresh builds over stale siblings.
  • sha256 must be the lowercase hex digest of the artifact bytes. Mismatch aborts the update and removes the partial file.

Public manifest API ​

The website exposes the current distribution manifest without a license key:

text
GET /api/updates/manifest

The response wraps the distribution document as { "manifest": { ... } }. This public endpoint is rate-limited and returns HTTP 502 if the distribution manifest is unavailable. It does not select versions by license or maintain a release catalog. Paid subscription access is checked separately from downloads. For a direct manifest URL used by update tools, use https://dl.clusto.app/manifest.json.

Release channels (semi-stable / unstable) ​

Updates are published on two channels that share one accumulating folder of versioned binaries - only the manifest each channel points at differs:

ChannelManifest fileWho tracks itWhen it moves
semi-stable (default)manifest.jsonthe whole fleet, unless overriddenonly on an explicit promote
unstablemanifest-unstable.jsonmachines opted in to newest buildsevery deploy distribution

manifest.json is also what the nginx index serves, so a base URL with no filename (https://clusto.app/) resolves to the semi-stable channel.

https://dl.clusto.app/ is the only distribution origin that advances. New releases, promotions, manifests, installers, and artifact files publish there. The Clusto website at https://clusto.app/ resolves update requests to that canonical distribution.

Two-hop legacy bridge ​

https://dl.vazac.dev/ is a frozen compatibility bridge, not an alias for the canonical store. Its manifest.json and manifest-unstable.json are each written once at A1 1.14.5 and never advance again. Every artifact URL in those bridge manifests is absolute and points to https://dl.clusto.app/.

This gives a node whose stored endpoint still names the legacy origin a safe two-hop update:

  1. The A0 client fetches its selected channel from dl.vazac.dev and receives the frozen 1.14.5 manifest.
  2. It downloads the A1 daemon and matching CLI from dl.clusto.app and performs the layout handoff.
  3. A1 migrates the stored endpoint to the canonical Clusto origin.
  4. A second update check can then install the current release from dl.clusto.app, even when it is newer than 1.14.5.

Fresh installs use the canonical origin immediately. install.sh accepts CLUSTO_BASE_URL as its override; HIVE_BASE_URL remains only as a temporary environment-name fallback. The legacy domain is retained solely as a fixed compatibility identifier for old clients.

Nodes and clients pick a channel rather than a raw URL:

  • Node: the [update].channel field in config.toml drives clustod's own self-update and its auto-check broadcast. Edit it from Settings → Updates → Update source; it replicates to peers.
  • Client (app): the channel is stored as a global preference (hive-update-channel) so it rides the cluster-preferences push/pull alongside every other setting. Edit it from Settings → Updates.

Publishing and promoting ​

bash
# Build, then publish the new version to the UNSTABLE channel:
pwsh ./scripts/clusto.ps1 build all
pwsh ./scripts/clusto.ps1 deploy distribution   # writes manifest-unstable.json

# Once an unstable build is vetted, promote it to semi-stable:
pwsh ./scripts/clusto.ps1 promote               # copies -> manifest.json

deploy distribution always writes manifest-unstable.json (newest version per artifact) and, on the very first publish, seeds manifest.json from it so existing installs keep resolving. After that, manifest.json only changes when you run promote, which copies the current unstable manifest verbatim - the versioned artifacts it references already live in the folder, so no rebuild is needed.

App updates (Settings → Updates → App updates) ​

Settings has a standalone Updates tab. Its top card, App updates, drives this app's own self-update from the connected daemon and is built for unattended, interruption-tolerant downloads:

  • Background, resumable download. Click Download (or enable auto-download) and the artifact streams to disk in the background. The bytes are saved to a <filename>.partial sidecar as they arrive; if the transfer is interrupted (you leave, the socket drops, the app restarts), the next attempt sends an HTTP Range request and resumes from where it left off instead of starting over. The daemon's /clients/{kind}/{platform} endpoint advertises Accept-Ranges: bytes and answers 206 Partial Content to make this possible.
  • Staged reuse - download once, install later. A completed, SHA-256 verified download is kept on disk. When the same update is rediscovered (on reconnect or relaunch) the app detects the staged file and jumps straight to Ready to install - so a cancelled or deferred install never re-downloads.
  • Auto-download toggle. Off by default. When on, a newly discovered update is fetched in the background automatically so Install is instant. The preference is stored per device in localStorage.
  • Install applies the staged artifact via the platform installer (NSIS on Windows, DMG on macOS, .deb/.rpm/tarball/AppImage on Linux, system PackageInstaller on Android). See Linux desktop artifacts for how the Linux app picks which one it updates with.

The same staged + resumable download powers the auto Install now banner, so banner installs are equally interruption-tolerant.

Persistent settings (Settings → Updates → Update source) ​

The Update source card (below App updates in the same tab) persists the manifest URL, channel, optional bearer token, auto-check toggle, and interval across restarts. This card is the only place these fields are edited - the cluster config dialog no longer duplicates them.

  • When connected to a daemon: Save writes to the daemon's [update] section in config.toml and broadcasts to every cluster peer via PeerMessage::SyncRemoteUpdateConfig. Each node ends up with the same URL/token in its local config, so a peer can still self-update on schedule even when other nodes are offline.
  • When not connected: Save stores the settings in browser localStorage only. The app keeps polling the URL locally so the desktop client can still receive an "update available" banner.

The token is propagated alongside the URL because cluster peers already share the cluster_token over the same authenticated peer transport - treat the manifest token with the same care.

Updating an installed node from the app ​

Connect to the installed daemon, then use Cluster > Check Updates and the node's Update from URL action. This works for a daemon on the same machine or a remote server. Update All updates eligible nodes across the cluster.

The daemon verifies and stages its own binary and the matching CLI, then restarts and installs the CLI beside the daemon. A missing or invalid CLI artifact fails the update before restart; a failure to install the staged CLI aborts the new daemon startup. A0 also verifies or creates its immutable rollback backup before allowing its process image to be replaced.

The app does not install or update a disconnected local service directly. Install the daemon through the install script and CLI wizard, and use the service tools on its host if it cannot start or accept a connection.

[update] section in config.toml ​

toml
[update]
manifest_url = "https://updates.example.com/clusto/"  # base directory URL
channel = "semi-stable"                              # or "unstable"
token = "optional-bearer-token"
auto_check = true
interval_secs = 21600    # 6 hours

manifest_url is the base directory; the concrete manifest file is derived from channel (see Release channels). A base that already ends in a *.json filename has that segment replaced, so both https://host/ and https://host/manifest.json resolve correctly.

Missing section → defaults to auto_check = true, interval_secs = 21600, channel = "semi-stable", base URL https://clusto.app/.

Auto-check + manual install confirmation ​

When auto_check = true and manifest_url is set, clustod spawns a background task at startup that:

  1. Sleeps for interval_secs.
  2. Fetches the manifest, compares its version against env!("CARGO_PKG_VERSION").
  3. If newer, broadcasts ServerMessage::RemoteUpdateAvailable to every connected client. The desktop app surfaces an "Install on this device / Update connected daemon" banner - the install itself is always manual: clustod never auto-applies an update without explicit user confirmation.

Interval changes propagate on the next tick (no restart needed). Settings re-read every cycle, so cluster-replicated changes via SyncRemoteUpdateConfig take effect within interval_secs on every node.

Endpoints ​

All three targets accept an optional Bearer token via CLUSTO_UPDATE_TOKEN env var (CLI) or the dialog (desktop). When set, every HTTP request to the manifest URL and to each artifact gets Authorization: Bearer <token>. Public URLs work with no token.

CLI self-update from URL ​

bash
# Default channel (semi-stable) from the default base URL:
clusto update

# Follow the unstable channel instead:
clusto update --channel unstable

# Custom base URL, with a bearer token:
CLUSTO_UPDATE_TOKEN=secret clusto update --from https://updates.example.com/clusto/
# or
clusto update --from https://... --remote-token secret

--from is the base directory URL and --channel selects the manifest file. When either flag is omitted, the CLI reads the local clustod config's [update] section and follows the same manifest_url and channel the node itself tracks (falling back to https://clusto.app/ and semi-stable when there is no config). --remote-token likewise falls back to the config's [update].token. So a plain clusto update on an unstable-channel node pulls the unstable CLI with no flags. The CLI downloads the cli artifact for its own platform, verifies the SHA-256, replaces the running binary, and re-execs.

Daemon self-update from URL ​

The daemon doesn't poll on its own - it self-updates when asked, via the WebSocket. You can drive that from the CLI:

bash
clusto update-node --from https://updates.example.com/clusto/manifest.json

A bare clusto update-node updates the local node, following the [update] manifest URL and channel the node already tracks. Every run requires the same-platform daemon and CLI entries, verifies and stages both, and installs or updates the CLI as part of the daemon restart. If either required artifact is missing or fails verification, the node does not restart into a partial update. If the staged CLI cannot be installed beside the daemon on the next boot, daemon startup fails rather than accepting a mixed release.

Add --refresh-artifacts to additionally download the remaining entries: CLIs and daemons for other platforms plus app installers and Android APKs. These are written into the daemon's local clients/ and daemons/ trees so connected clients and peer relays can pull them later. The current node's CLI is always staged and installed even when this flag is omitted. Each peer daemon is brought up to date by running its own URL update against the same manifest.

The daemon side never auto-polls. Trigger an update explicitly - cron, systemd timer, a CI deploy step, or a human at the dialog.

Desktop app update from URL ​

Set the manifest URL, channel, and optional bearer token under Settings > Updates > Update source. Use App updates to update the current app, or Cluster > Update from URL on the target node to update its daemon and matching CLI over the authenticated connection. The desktop app does not stage artifacts for other platforms; use clusto update-node --refresh-artifacts when a daemon should cache the remaining manifest entries.

On desktop the installer is downloaded into the user's Downloads folder and run automatically (NSIS /S on Windows, open on macOS, pkexec apt install -y for .deb, pkexec rpm -Uvh for .rpm, pkexec tar -xzf … -C / or replace-and-restart for the tarball, replace-and-restart for AppImage). On Android the APK is staged into the app cache and handed to the system PackageInstaller via the JS bridge.

Linux desktop artifacts ​

Linux ships four desktop artifacts per release, all x64:

ArtifactManifest platformWebKitGTK
Clusto-linux-x64-<ver>.deblinux-x64-debsystem webkit2gtk-4.1
Clusto-linux-x64-<ver>.rpmlinux-x64-rpmsystem webkit2gtk-4.1
Clusto-linux-x64-<ver>.tar.gzlinux-x64-tarsystem webkit2gtk-4.1
Clusto-linux-x64-<ver>.AppImagelinux-x64bundled (Ubuntu 24.04)

Prefer the package or the tarball. The AppImage carries its own WebKitGTK plus that build's EGL/wayland libraries, and those crash against a much newer host Mesa: the WebKit process dies with EGL_BAD_PARAMETER and the window never paints (seen on RDNA4 + Mesa 26). The other three link against the system webkit2gtk-4.1, so they follow the host's graphics stack. The AppImage stays published only for distros with no system WebKitGTK 4.1.

The tarball is the .deb payload repacked (./usr/bin, ./usr/share), so a system-wide install is sudo tar -xzf Clusto-linux-x64-<ver>.tar.gz -C /; it can also be unpacked into any user-owned prefix and run from there.

The running app reports which artifact it wants as its update platform: an AppImage (it sets APPIMAGE in its own environment) asks for linux-x64, otherwise the app picks linux-x64-deb when dpkg is present, linux-x64-rpm when rpm is, and linux-x64-tar as the fallback. An install therefore updates with the same artifact type it was installed from.

URL updates from the Cluster view ​

The Cluster view's toolbar exposes update actions once a manifest URL is configured under Settings → Updates → Update source:

  • Check Updates fetches the configured manifest and refreshes the per-node buttons so you can see which nodes are behind.

  • Update All hands the whole run to the connected daemon in a single message. The daemon fetches the manifest, picks every node whose running daemon version is older than it, updates the peers, and self-updates last. Each node stages its matching daemon and CLI before it restarts. It reports "All nodes already on vX.Y.Z or newer" when nothing is behind, and lists how many offline nodes it skipped.

    The run lives on the daemon, not in the app: you can close the client right after clicking and the cluster still finishes the update. Progress arrives as one acknowledgement per node, broadcast to every connected client, so a second app (or a reconnecting one) sees the results too. The same is true of a single node's Update from URL - the download and staging survive a disconnect.

Each node card also shows an Update from URL (vX.Y.Z) button - the connected node and each peer - once a manifest version newer than that node is known. The manifest version is populated by Check Updates, the Check now button under Settings → Updates → Update source, the daemon's auto-check broadcast, or the client-side poll.

Node cards still report the installed CLI version, but there is no separate install or update button. Update from URL updates the daemon and installs or updates the matching CLI beside clustod in the same operation.

Triggering an update asks the connected daemon to drive it:

  • Connected node: the daemon runs the same flow as the update-node --from CLI path locally.
  • Peer: the connected daemon forwards a SelfUpdateFromUrl over the inter-node WebSocket. The peer fetches the manifest itself, picks the daemon and CLI artifacts for its own platform, verifies both, and restarts - the manifest URL/token travel over the authenticated peer transport, the binary bytes never do. A manifest missing either artifact for the peer's platform comes back as a failed update acknowledgement.

Because every node downloads from the manifest URL directly, no node needs to host binaries for another node's platform.

Both paths run in a detached daemon task. The client's message only starts the work; the acknowledgement comes back over a broadcast channel when the node actually finishes, which is why closing the app mid-update no longer cancels it. Peers are updated concurrently, and the connected node updates last so it stays alive to dispatch and report on the others before its own restart.

Producing a manifest ​

A simple shell helper to generate manifest.json from a dist/ folder:

bash
sha() { sha256sum "$1" | cut -d' ' -f1; }
sz() { stat -c%s "$1"; }
cat > manifest.json <<EOF
{
  "version": "$VERSION",
  "build_date": "$(date -u +%FT%TZ)",
  "artifacts": [
    {"kind":"daemon","platform":"linux-x64","filename":"clustod","sha256":"$(sha clustod)","size":$(sz clustod),"url":"clustod"},
    {"kind":"cli","platform":"linux-x64","filename":"clusto","sha256":"$(sha clusto)","size":$(sz clusto),"url":"clusto"}
  ]
}
EOF

In practice you don't hand-write this: pwsh ./scripts/clusto.ps1 deploy distribution stages the versioned binaries and generates the channel manifests for you (unstable on every run, semi-stable on promote). See Release channels. The snippet above is only useful for a one-off static endpoint.

Security model ​

  • TLS is the only end-to-end integrity check on the manifest: there is no manifest signature. Pick https:// URLs from a host you control, or put the static folder behind an authenticated endpoint and use the bearer token.
  • SHA-256 is verified on every artifact after download; mismatch aborts the install and deletes the staged file. Tampering with bytes alone does not produce a working install.
  • No replay protection: a stale manifest still installs cleanly as long as the SHA-256 matches. If you publish a vulnerable older version under the same manifest URL and don't tighten access, clients pointing at that URL will install it. Treat the manifest URL like any other deployment surface.

Pre-existing alternatives ​

The remote-URL flow doesn't replace the existing update paths - it sits beside them:

  • clusto update (no --from): pull from the daemon you're connected to.
  • Cluster view → "Update peer from this node": daemon-to-daemon relay over the inter-node WebSocket. No HTTP round-trip; bytes never leave the cluster. The receiving daemon writes the synced artifacts into its local clients/ and daemons/ trees next to clustod; on Windows installs (C:\Program Files\Clusto) the install step pre-creates those dirs and grants BUILTIN\Users Modify so the relay succeeds even when clustod is not running as the elevated LocalSystem service. A relay failing with Access is denied. (os error 5) means the target was installed before this provisioning existed - re-run the node install to repair the ACLs.
  • Auto-banner in the desktop app: triggered when the connected daemon's bundled artifact is newer than the running build.

Use the remote-URL flow when there is no upstream daemon to pull from, or when you want one URL to be the source of truth across an unmanaged fleet.

Clusto - remote AI coding agents over WebSocket.