mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
iOS Security Stack 3/5: Runtime Security Guards (#33031)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 9917165401
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -90,7 +90,9 @@ final class NodeAppModel {
|
||||
var lastShareEventText: String = "No share events yet."
|
||||
var openChatRequestID: Int = 0
|
||||
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var queuedAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var lastAgentDeepLinkPromptAt: Date = .distantPast
|
||||
@ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task<Void, Never>?
|
||||
|
||||
// Primary "node" connection: used for device capabilities and node.invoke requests.
|
||||
private let nodeGateway = GatewayNodeSession()
|
||||
@@ -2591,19 +2593,31 @@ extension NodeAppModel {
|
||||
"agent deep link rejected: unkeyed message too long chars=\(message.count, privacy: .public)")
|
||||
return
|
||||
}
|
||||
if Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt) < 1.0 {
|
||||
self.deepLinkLogger.debug("agent deep link prompt throttled")
|
||||
return
|
||||
}
|
||||
self.lastAgentDeepLinkPromptAt = Date()
|
||||
|
||||
let urlText = originalURL.absoluteString
|
||||
let prompt = AgentDeepLinkPrompt(
|
||||
id: UUID().uuidString,
|
||||
messagePreview: message,
|
||||
urlPreview: urlText.count > 500 ? "\(urlText.prefix(500))…" : urlText,
|
||||
request: self.effectiveAgentDeepLinkForPrompt(link))
|
||||
self.pendingAgentDeepLinkPrompt = prompt
|
||||
|
||||
let promptIntervalSeconds = 5.0
|
||||
let elapsed = Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt)
|
||||
if elapsed < promptIntervalSeconds {
|
||||
if self.pendingAgentDeepLinkPrompt != nil {
|
||||
self.pendingAgentDeepLinkPrompt = prompt
|
||||
self.recordShareEvent("Updated local confirmation request (\(message.count) chars).")
|
||||
self.deepLinkLogger.debug("agent deep link prompt coalesced into active confirmation")
|
||||
return
|
||||
}
|
||||
|
||||
let remaining = max(0, promptIntervalSeconds - elapsed)
|
||||
self.queueAgentDeepLinkPrompt(prompt, initialDelaySeconds: remaining)
|
||||
self.recordShareEvent("Queued local confirmation (\(message.count) chars).")
|
||||
self.deepLinkLogger.debug("agent deep link prompt queued due to rate limit")
|
||||
return
|
||||
}
|
||||
|
||||
self.presentAgentDeepLinkPrompt(prompt)
|
||||
self.recordShareEvent("Awaiting local confirmation (\(message.count) chars).")
|
||||
self.deepLinkLogger.info("agent deep link requires local confirmation")
|
||||
return
|
||||
@@ -2672,6 +2686,60 @@ extension NodeAppModel {
|
||||
self.deepLinkLogger.info("agent deep link cancelled by local user")
|
||||
}
|
||||
|
||||
private func presentAgentDeepLinkPrompt(_ prompt: AgentDeepLinkPrompt) {
|
||||
self.lastAgentDeepLinkPromptAt = Date()
|
||||
self.pendingAgentDeepLinkPrompt = prompt
|
||||
}
|
||||
|
||||
private func queueAgentDeepLinkPrompt(_ prompt: AgentDeepLinkPrompt, initialDelaySeconds: TimeInterval) {
|
||||
self.queuedAgentDeepLinkPrompt = prompt
|
||||
guard self.queuedAgentDeepLinkPromptTask == nil else { return }
|
||||
|
||||
self.queuedAgentDeepLinkPromptTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let delayNs = UInt64(max(0, initialDelaySeconds) * 1_000_000_000)
|
||||
if delayNs > 0 {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: delayNs)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
await self.deliverQueuedAgentDeepLinkPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
private func deliverQueuedAgentDeepLinkPrompt() async {
|
||||
defer { self.queuedAgentDeepLinkPromptTask = nil }
|
||||
let promptIntervalSeconds = 5.0
|
||||
while let prompt = self.queuedAgentDeepLinkPrompt {
|
||||
if self.pendingAgentDeepLinkPrompt != nil {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 200_000_000)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let elapsed = Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt)
|
||||
if elapsed < promptIntervalSeconds {
|
||||
let remaining = max(0, promptIntervalSeconds - elapsed)
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
self.queuedAgentDeepLinkPrompt = nil
|
||||
self.presentAgentDeepLinkPrompt(prompt)
|
||||
self.recordShareEvent("Awaiting local confirmation (\(prompt.messagePreview.count) chars).")
|
||||
self.deepLinkLogger.info("agent deep link queued prompt delivered")
|
||||
}
|
||||
}
|
||||
|
||||
private func submitAgentDeepLink(_ link: AgentDeepLink, messageCharCount: Int) async {
|
||||
do {
|
||||
try await self.sendAgentRequest(link: link)
|
||||
|
||||
@@ -987,9 +987,12 @@ final class TalkModeManager: NSObject {
|
||||
self.logger.warning("unknown voice alias \(requestedVoice ?? "?", privacy: .public)")
|
||||
}
|
||||
|
||||
let resolvedKey =
|
||||
(self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil) ??
|
||||
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
|
||||
let configuredKey = self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil
|
||||
#if DEBUG
|
||||
let resolvedKey = configuredKey ?? ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
|
||||
#else
|
||||
let resolvedKey = configuredKey
|
||||
#endif
|
||||
let apiKey = resolvedKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let preferredVoice = resolvedVoice ?? self.currentVoiceId ?? self.defaultVoiceId
|
||||
let voiceId: String? = if let apiKey, !apiKey.isEmpty {
|
||||
@@ -1483,9 +1486,12 @@ final class TalkModeManager: NSObject {
|
||||
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
|
||||
}
|
||||
|
||||
let resolvedKey =
|
||||
(self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil) ??
|
||||
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
|
||||
let configuredKey = self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil
|
||||
#if DEBUG
|
||||
let resolvedKey = configuredKey ?? ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
|
||||
#else
|
||||
let resolvedKey = configuredKey
|
||||
#endif
|
||||
let apiKey = resolvedKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let voiceId: String? = if let apiKey, !apiKey.isEmpty {
|
||||
await self.resolveVoiceId(preferred: preferredVoice, apiKey: apiKey)
|
||||
|
||||
@@ -416,6 +416,20 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
|
||||
#expect(appModel.openChatRequestID == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleDeepLinkCoalescesPromptWhenRateLimited() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
appModel._test_setGatewayConnected(true)
|
||||
|
||||
await appModel.handleDeepLink(url: makeAgentDeepLinkURL(message: "first prompt"))
|
||||
let firstPrompt = try #require(appModel.pendingAgentDeepLinkPrompt)
|
||||
|
||||
await appModel.handleDeepLink(url: makeAgentDeepLinkURL(message: "second prompt"))
|
||||
let coalescedPrompt = try #require(appModel.pendingAgentDeepLinkPrompt)
|
||||
|
||||
#expect(coalescedPrompt.id != firstPrompt.id)
|
||||
#expect(coalescedPrompt.messagePreview.contains("second prompt"))
|
||||
}
|
||||
|
||||
@Test @MainActor func handleDeepLinkStripsDeliveryFieldsWhenUnkeyed() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
appModel._test_setGatewayConnected(true)
|
||||
|
||||
Reference in New Issue
Block a user