Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannelSupport.swift
T
Peter SteinbergerandGitHub d7de67ae02 feat: ask_user follow-ups — harness convergence, channel finalization + reactions, native cards, docked web panel (#110372)
* feat(ask-user): follow-up harness slice

* feat(ask-user): follow-up channels slice

* feat(ask-user): follow-up native slice

* feat(ui): dock question panel above composer with stepper and compact stream summaries

* docs: refresh follow-up integration maps

* test(ui): align terminal summary proof

* fix(infra): echo declared option answers in terminal status when free-text is allowed

* fix(infra): keep reaction answering when display labels are formatter-adjusted

* fix(agents): settle plain-text claims only after question registration commits

* fix(agents,apps): commit-ordered claim persistence, claim-aware prompt delivery, non-blocking question refresh

* fix(harness,infra): reaction-appropriate question copy, caller presentations honored

* fix(native,infra,agents): local-expiry eviction, value-addressed reactions, reserve-before-request

* fix(infra,android): dual-mode question resolver for compact callbacks; reset terminal retention on replayed pending

* fix(harness,discord): claim-aware prompt delivery in run helper; escape finalization labels

* fix(macos): merge transient-content visibility with question cards after main sync

* fix: repair ask user follow-up CI

* test: update limited bootstrap scope expectation

* fix: retain shared question card API

* chore: refresh native i18n inventory
2026-07-18 03:32:39 -04:00

98 lines
3.1 KiB
Swift

import CryptoKit
import Foundation
func gatewayIntValue(_ value: Any?) -> Int? {
if let value = value as? Int {
return value
}
if let value = value as? Int64 {
return Int(exactly: value)
}
if let value = value as? Double, value.rounded() == value {
return Int(exactly: value)
}
if let value = value as? NSNumber, CFGetTypeID(value) != CFBooleanGetTypeID() {
let doubleValue = value.doubleValue
guard doubleValue.rounded() == doubleValue else {
return nil
}
return Int(exactly: doubleValue)
}
if let value = value as? String {
return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
}
return nil
}
/// Bridges task cancellation into the request continuation without racing send.
final class GatewayRequestCancellationGate: @unchecked Sendable {
private let lock = NSLock()
private var cancelled = false
var isCancelled: Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.cancelled
}
func cancel() {
self.lock.lock()
self.cancelled = true
self.lock.unlock()
}
}
extension GatewayChannelActor {
enum ConnectChallengeError: Error {
case timeout
}
public static let defaultOperatorConnectScopes: [String] = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.questions",
"operator.pairing",
]
struct SelectedConnectAuth {
let authToken: String?
let authBootstrapToken: String?
let authDeviceToken: String?
let authPassword: String?
let signatureToken: String?
let storedToken: String?
let storedScopes: [String]?
let authSource: GatewayAuthSource
let suppressedDeviceTokenRetry: Bool
}
}
extension GatewayChannelActor.SelectedConnectAuth {
func makeAuthBinding(key: SymmetricKey?, deviceId: String?) -> GatewayAuthBinding {
let credentialFingerprint = key.map { key in
var values = [
self.authSource.rawValue,
deviceId ?? "",
]
if let authToken = self.authToken {
values.append(contentsOf: ["token", authToken])
if let authDeviceToken = self.authDeviceToken {
values.append(contentsOf: ["deviceToken", authDeviceToken])
}
} else if let authBootstrapToken = self.authBootstrapToken {
values.append(contentsOf: ["bootstrapToken", authBootstrapToken])
} else if let authPassword = self.authPassword {
values.append(contentsOf: ["password", authPassword])
}
let framed = values.map { "\($0.utf8.count):\($0)" }.joined(separator: "|")
let tag = HMAC<SHA256>.authenticationCode(for: Data(framed.utf8), using: key)
return tag.map { String(format: "%02x", $0) }.joined()
}
return GatewayAuthBinding(
source: self.authSource,
credentialFingerprint: credentialFingerprint)
}
}