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:
parent
1b03d23fee
commit
ccd84bd578
4 changed files with 148 additions and 34 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue