Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ extension AppDelegate {
self.hostBrowserExecutor.cancel(msg.requestId)

case .hostTransferRequest(let msg):
let localClientId = DeviceIdStore.getOrCreate()
let isLocalConversation = self.mainWindow?.conversationManager
.conversations.contains(where: { $0.conversationId == msg.conversationId }) ?? false
let isTargeted = msg.targetClientId == localClientId
let isUntargetedLocal = msg.targetClientId == nil && isLocalConversation
guard isTargeted || isUntargetedLocal else { break }
Comment on lines +450 to +454
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not gate untargeted transfers on UI conversation cache

The new hostTransferRequest guard drops untargeted requests unless mainWindow?.conversationManager.conversations already contains the conversation ID, but that UI cache can lag behind transport ownership (or be unavailable when mainWindow is nil). EventStreamClient already admits untargeted host-tool messages only for locallyOwnedConversationIds, so this extra UI-based check can reject legitimate local transfer requests and cause transfers to stall/fail in early/new/background conversation flows.

Useful? React with 👍 / 👎.

HostToolExecutor.executeHostTransferRequest(msg)
case .hostTransferCancel(let msg):
HostToolExecutor.cancelHostTransferRequest(msg.requestId)
Expand Down
1 change: 1 addition & 0 deletions clients/shared/Network/EventStreamClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ public final class EventStreamClient {
log.warning("Ignoring host_browser_request for non-local conversation \(msg.conversationId, privacy: .public)")
return true
case .hostTransferRequest(let msg):
if msg.targetClientId != nil { return false } // pass through targeted requests
if locallyOwnedConversationIds.contains(msg.conversationId) { return false }
log.warning("Ignoring host_transfer_request for non-local conversation \(msg.conversationId, privacy: .public)")
return true
Expand Down
4 changes: 2 additions & 2 deletions clients/shared/Network/GatewayHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public enum GatewayHTTPClient {
/// - quiet: When `true`, suppresses HTTP request/response logging for this request.
/// - Returns: A `Response` with the raw data and HTTP status code.
/// - Throws: `ClientError` if the request cannot be constructed, or network errors from `URLSession`.
public static func get(path: String, params: [String: String]? = nil, timeout: TimeInterval = 30, quiet: Bool = false, unprefixed: Bool = false) async throws -> Response {
return try await executeWithRetry(path: path, params: params, method: "GET", timeout: timeout, quiet: quiet, unprefixed: unprefixed)
public static func get(path: String, params: [String: String]? = nil, timeout: TimeInterval = 30, quiet: Bool = false, unprefixed: Bool = false, extraHeaders: [String: String]? = nil) async throws -> Response {
return try await executeWithRetry(path: path, params: params, method: "GET", timeout: timeout, quiet: quiet, unprefixed: unprefixed, configure: extraHeaders.map { h in { req in for (k, v) in h { req.setValue(v, forHTTPHeaderField: k) } } })
}

/// Performs an authenticated GET request and decodes the JSON response into the given type.
Expand Down
6 changes: 4 additions & 2 deletions clients/shared/Network/HostProxyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public struct HostProxyClient: HostProxyClientProtocol {
let response = try await GatewayHTTPClient.post(
path: "host-transfer-result",
body: body,
extraHeaders: ["X-Vellum-Client-Id": DeviceIdStore.getOrCreate()],
timeout: timeout
)
guard response.isSuccess else {
Expand All @@ -153,7 +154,8 @@ public struct HostProxyClient: HostProxyClientProtocol {
// Use a generous timeout — large files may take a while to download.
let response = try await GatewayHTTPClient.get(
path: "transfers/\(transferId)/content",
timeout: 300
timeout: 300,
extraHeaders: ["X-Vellum-Client-Id": DeviceIdStore.getOrCreate()]
)
guard response.isSuccess else {
throw TransferError.pullFailed(statusCode: response.statusCode)
Expand All @@ -169,7 +171,7 @@ public struct HostProxyClient: HostProxyClientProtocol {
body: data,
params: ["sourcePath": sourcePath],
contentType: "application/octet-stream",
extraHeaders: ["X-Transfer-SHA256": sha256],
extraHeaders: ["X-Transfer-SHA256": sha256, "X-Vellum-Client-Id": DeviceIdStore.getOrCreate()],
timeout: timeout
)
guard response.isSuccess else {
Expand Down
4 changes: 4 additions & 0 deletions clients/shared/Network/MessageTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2044,11 +2044,15 @@ public struct HostTransferRequest: Decodable, Sendable {
public let sizeBytes: Int?
public let sha256: String?
public let overwrite: Bool?
/// When set, this request is targeted at a specific client ID. Non-nil only for
/// cross-client proxy requests routed through HostTransferProxy.
public let targetClientId: String?

private enum CodingKeys: String, CodingKey {
case type, requestId, conversationId, direction
case transferId, destPath, sourcePath, sizeBytes
case sha256, overwrite
case targetClientId
}
}

Expand Down