Fix nightly SSH remote daemon checksum mismatch (#2225)

* Fix nightly SSH remote daemon checksum mismatch

Each nightly build overwrites the shared cmuxd-remote-* assets on the
nightly release, but older nightly DMGs have manifests with checksums
from their build time. When a user's nightly is even one build behind,
the downloaded binary doesn't match their embedded manifest.

Two-layer fix:

1. CI: version nightly remote daemon asset names with the build number
   (e.g. cmuxd-remote-darwin-arm64-2362248028801) so each nightly's
   manifest points to immutable files. Unsuffixed "latest" copies are
   still uploaded for tooling compatibility.

2. Client: on checksum mismatch, fetch the live manifest from the
   release and verify against that. This handles users on older
   nightlies that predate the CI fix.

Fixes https://github.com/manaflow-ai/cmux/issues/1745

* Fix unsuffixed checksums file to use generic filenames

Regenerate cmuxd-remote-checksums.txt from the unsuffixed alias
binaries so `shasum -c` works against the generic asset names.
Also document that unsuffixed manifest intentionally keeps versioned
downloadURLs and that aliases don't carry attestation.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-26 17:24:37 -07:00 committed by GitHub
parent 1b03d23fee
commit ccd84bd578
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 148 additions and 34 deletions

View file

@ -4146,7 +4146,29 @@ final class WorkspaceRemoteSessionController {
.appendingPathComponent("cmuxd-remote", isDirectory: false)
}
private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String) throws -> URL {
/// Fetch the live manifest JSON from the release, returning nil on any failure.
private static func fetchRemoteManifestLocked(releaseURL: String, version: String) -> WorkspaceRemoteDaemonManifest? {
guard let manifestURL = URL(string: "\(releaseURL)/cmuxd-remote-manifest.json") else { return nil }
let request = NSMutableURLRequest(url: manifestURL)
request.timeoutInterval = 15
request.setValue("cmux/\(version)", forHTTPHeaderField: "User-Agent")
let session = URLSession(configuration: .ephemeral)
let semaphore = DispatchSemaphore(value: 0)
var resultData: Data?
session.dataTask(with: request as URLRequest) { data, response, error in
defer { semaphore.signal() }
guard error == nil,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else { return }
resultData = data
}.resume()
_ = semaphore.wait(timeout: .now() + 20.0)
session.finishTasksAndInvalidate()
guard let data = resultData else { return nil }
return try? JSONDecoder().decode(WorkspaceRemoteDaemonManifest.self, from: data)
}
private func downloadRemoteDaemonBinaryLocked(entry: WorkspaceRemoteDaemonManifest.Entry, version: String, releaseURL: String? = nil) throws -> URL {
guard let url = URL(string: entry.downloadURL) else {
throw NSError(domain: "cmux.remote.daemon", code: 25, userInfo: [
NSLocalizedDescriptionKey: "remote daemon manifest has an invalid download URL",
@ -4193,10 +4215,21 @@ final class WorkspaceRemoteSessionController {
}
let downloadedSHA = try Self.sha256Hex(forFile: downloadedURL)
guard downloadedSHA == entry.sha256.lowercased() else {
throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [
NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)",
])
if downloadedSHA != entry.sha256.lowercased() {
// The embedded manifest's checksum doesn't match the downloaded binary.
// This can happen when a newer nightly overwrites the shared release
// asset after this build's manifest was embedded. As a fallback, fetch
// the live manifest from the release and verify against that.
if let releaseURL,
let liveManifest = Self.fetchRemoteManifestLocked(releaseURL: releaseURL, version: version),
let liveEntry = liveManifest.entry(goOS: entry.goOS, goArch: entry.goArch),
downloadedSHA == liveEntry.sha256.lowercased() {
debugLog("remote.download.checksum-fallback: embedded manifest checksum stale, live manifest matched for \(entry.assetName)")
} else {
throw NSError(domain: "cmux.remote.daemon", code: 28, userInfo: [
NSLocalizedDescriptionKey: "remote daemon checksum mismatch for \(entry.assetName)",
])
}
}
let tempURL = cacheURL.deletingLastPathComponent()
@ -4229,7 +4262,7 @@ final class WorkspaceRemoteSessionController {
}
try? FileManager.default.removeItem(at: cacheURL)
}
let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion)
let downloadedURL = try downloadRemoteDaemonBinaryLocked(entry: entry, version: manifest.appVersion, releaseURL: manifest.releaseURL)
debugLog("remote.build.downloaded path=\(downloadedURL.path)")
return downloadedURL
}