clients/swift: fix Linux compile — URLSession async API isn't on swift-corelibs-foundation

The audit-fix landing at 7e878e6f assumed URLSession.data(for:) and
URLSession.upload(for:fromFile:) are available on Linux Swift 5.9+.
They aren't — those are Apple-platform-only (macOS 12+, iOS 15+).
Linux's swift-corelibs-foundation only exposes the callback-style API.

Fix: private URLSession.forgeData(for:) / forgeUpload(for:fromFile:)
helpers on URLSession that bridge the callback API via
withCheckedThrowingContinuation on Linux (#if canImport(FoundationNetworking))
and forward to the native async API on Apple. 3 call sites updated.

Also added @preconcurrency to Foundation imports — URL, URLSession,
JSONEncoder, JSONDecoder are not Sendable on Linux Foundation;
@preconcurrency suppresses the spurious warnings without changing
runtime behavior.

Verified locally on Linux Swift 5.9.2 inside the crafting-table image
(7.8GB monolith with the full toolchain). The audit-fix agent that
shipped 7e878e6f was running without a swift toolchain and explicitly
flagged this verification was needed; this commit closes that.
This commit is contained in:
Kayos 2026-04-29 13:16:42 -07:00
parent dbbead261d
commit aeb3c30097

View file

@ -12,16 +12,68 @@
// `URLSession.data(for:)` / `URLSession.upload(for:fromFile:)`, so wrapping // `URLSession.data(for:)` / `URLSession.upload(for:fromFile:)`, so wrapping
// a call in `Task { ... }.cancel()` cleanly aborts the in-flight request. // a call in `Task { ... }.cancel()` cleanly aborts the in-flight request.
// //
// Linux: builds against swift-corelibs-foundation. The async URLSession // Linux: builds against swift-corelibs-foundation. As of Swift 5.9.2 the
// methods used here (`data(for:)`, `upload(for:fromFile:)`) are available // async `URLSession.data(for:)` / `URLSession.upload(for:fromFile:)` are
// on Linux as of Swift 5.9. // **only** on Apple platforms (macOS 12+, iOS 15+, etc.) Linux's
// FoundationNetworking exposes the callback-style URLSession API only.
// The `forgeData(for:)` / `forgeUpload(for:fromFile:)` helpers below
// bridge the callback API with `withCheckedThrowingContinuation` on Linux
// and forward to the native async API on Apple.
//
// `@preconcurrency` on the Foundation imports suppresses Sendable warnings
// for URL / URLSession / JSONEncoder / JSONDecoder which are not declared
// Sendable on swift-corelibs-foundation; the SDK still preserves Sendable
// semantics by the way it uses them (one shared session, immutable
// captured fields).
import Foundation @preconcurrency import Foundation
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
import FoundationNetworking @preconcurrency import FoundationNetworking
#endif #endif
// MARK: - URLSession Linux/Apple bridge -----------------------------------
extension URLSession {
/// Async `(Data, URLResponse)` for a request uses the native API on
/// Apple, bridges the callback API on Linux. Behaves identically.
fileprivate func forgeData(for request: URLRequest) async throws -> (Data, URLResponse) {
#if canImport(FoundationNetworking)
return try await withCheckedThrowingContinuation { cont in
let task = self.dataTask(with: request) { data, response, error in
if let error { cont.resume(throwing: error); return }
guard let data, let response else {
cont.resume(throwing: URLError(.unknown)); return
}
cont.resume(returning: (data, response))
}
task.resume()
}
#else
return try await self.data(for: request)
#endif
}
/// Async upload from a file uses the native API on Apple, bridges
/// the callback API on Linux.
fileprivate func forgeUpload(for request: URLRequest, fromFile fileURL: URL) async throws -> (Data, URLResponse) {
#if canImport(FoundationNetworking)
return try await withCheckedThrowingContinuation { cont in
let task = self.uploadTask(with: request, fromFile: fileURL) { data, response, error in
if let error { cont.resume(throwing: error); return }
guard let data, let response else {
cont.resume(throwing: URLError(.unknown)); return
}
cont.resume(returning: (data, response))
}
task.resume()
}
#else
return try await self.upload(for: request, fromFile: fileURL)
#endif
}
}
/// Thread-safe client for the clawdforge REST API. /// Thread-safe client for the clawdforge REST API.
/// ///
/// Construct once per (baseURL, token) pair and share across the app. /// Construct once per (baseURL, token) pair and share across the app.
@ -164,7 +216,7 @@ public struct ForgeClient: Sendable {
let (data, response): (Data, URLResponse) let (data, response): (Data, URLResponse)
do { do {
(data, response) = try await session.upload(for: req, fromFile: tempURL) (data, response) = try await session.forgeUpload(for: req, fromFile: tempURL)
} catch let urlError as URLError { } catch let urlError as URLError {
throw ForgeError.transport(urlError) throw ForgeError.transport(urlError)
} catch { } catch {
@ -275,7 +327,7 @@ public struct ForgeClient: Sendable {
let (data, response): (Data, URLResponse) let (data, response): (Data, URLResponse)
do { do {
(data, response) = try await session.data(for: req) (data, response) = try await session.forgeData(for: req)
} catch let urlError as URLError { } catch let urlError as URLError {
throw ForgeError.transport(urlError) throw ForgeError.transport(urlError)
} catch { } catch {
@ -291,7 +343,7 @@ public struct ForgeClient: Sendable {
let (data, response): (Data, URLResponse) let (data, response): (Data, URLResponse)
do { do {
(data, response) = try await session.data(for: req) (data, response) = try await session.forgeData(for: req)
} catch let urlError as URLError { } catch let urlError as URLError {
throw ForgeError.transport(urlError) throw ForgeError.transport(urlError)
} catch { } catch {