mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: harden macOS usage cost submenu recursion guard (#25341) (thanks @yingchunbai)
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- macOS/Menu bar: stop reusing the injector delegate for the "Usage cost (30 days)" submenu to prevent recursive submenu injection loops when opening cost history. (#25341) Thanks @yingchunbai.
|
||||
- Control UI/Chat images: harden image-open clicks against reverse tabnabbing by using opener isolation (`noopener,noreferrer` plus `window.opener = null`). (#18685) Thanks @Mariana-Codebase.
|
||||
- Security/iOS deep links: require local confirmation (or trusted key) before forwarding `openclaw://agent` requests from iOS to gateway `agent.request`, and strip unkeyed delivery-routing fields to reduce exfiltration risk. This ships in the next npm release. Thanks @GCXWLP for reporting.
|
||||
- Security/Export session HTML: escape raw HTML markdown tokens in the exported session viewer, harden tree/header metadata rendering against HTML injection, and sanitize image data-URL MIME types in export output to prevent stored XSS when opening exported HTML files. This ships in the next npm release. Thanks @allsmog for reporting.
|
||||
|
||||
@@ -446,6 +446,8 @@ extension MenuSessionsInjector {
|
||||
|
||||
private func buildUsageOverflowMenu(rows: [UsageRow], width: CGFloat) -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
// Keep submenu delegate nil: reusing the status-menu delegate here causes
|
||||
// recursive reinjection whenever this submenu is opened.
|
||||
for row in rows {
|
||||
let item = NSMenuItem()
|
||||
item.tag = self.tag
|
||||
@@ -1225,6 +1227,12 @@ extension MenuSessionsInjector {
|
||||
self.usageCacheUpdatedAt = Date()
|
||||
}
|
||||
|
||||
func setTestingCostUsageSummary(_ summary: GatewayCostUsageSummary?, errorText: String? = nil) {
|
||||
self.cachedCostSummary = summary
|
||||
self.cachedCostErrorText = errorText
|
||||
self.costCacheUpdatedAt = Date()
|
||||
}
|
||||
|
||||
func injectForTesting(into menu: NSMenu) {
|
||||
self.inject(into: menu)
|
||||
}
|
||||
|
||||
@@ -93,4 +93,45 @@ struct MenuSessionsInjectorTests {
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
||||
}
|
||||
|
||||
@Test func costUsageSubmenuDoesNotUseInjectorDelegate() {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(true)
|
||||
|
||||
let summary = GatewayCostUsageSummary(
|
||||
updatedAt: Date().timeIntervalSince1970 * 1000,
|
||||
days: 1,
|
||||
daily: [
|
||||
GatewayCostUsageDay(
|
||||
date: "2026-02-24",
|
||||
input: 10,
|
||||
output: 20,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 30,
|
||||
totalCost: 0.12,
|
||||
missingCostEntries: 0),
|
||||
],
|
||||
totals: GatewayCostUsageTotals(
|
||||
input: 10,
|
||||
output: 20,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 30,
|
||||
totalCost: 0.12,
|
||||
missingCostEntries: 0))
|
||||
injector.setTestingCostUsageSummary(summary, errorText: nil)
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
|
||||
injector.injectForTesting(into: menu)
|
||||
|
||||
let usageCostItem = menu.items.first { $0.title == "Usage cost (30 days)" }
|
||||
#expect(usageCostItem != nil)
|
||||
#expect(usageCostItem?.submenu != nil)
|
||||
#expect(usageCostItem?.submenu?.delegate == nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user