fix(protocol): preserve AnyCodable booleans from JSON bridge (#20220)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 1d86183e3b
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:
Mariano
2026-02-18 17:39:54 +00:00
committed by GitHub
parent 05173ec53a
commit e9b4d86e37
3 changed files with 53 additions and 3 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky.
- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky.
- Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky.
- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`<chatId>:topic:<threadId>`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi.

View File

@@ -6,13 +6,13 @@ import Foundation
public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
public let value: Any
public init(_ value: Any) { self.value = value }
public init(_ value: Any) { self.value = Self.normalize(value) }
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
if container.decodeNil() { self.value = NSNull(); return }
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
@@ -23,10 +23,12 @@ public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let boolVal as Bool: try container.encode(boolVal)
case let intVal as Int: try container.encode(intVal)
case let doubleVal as Double: try container.encode(doubleVal)
case let boolVal as Bool: try container.encode(boolVal)
case let stringVal as String: try container.encode(stringVal)
case let number as NSNumber where CFGetTypeID(number) == CFBooleanGetTypeID():
try container.encode(number.boolValue)
case is NSNull: try container.encodeNil()
case let dict as [String: AnyCodable]: try container.encode(dict)
case let array as [AnyCodable]: try container.encode(array)
@@ -51,6 +53,13 @@ public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
}
}
private static func normalize(_ value: Any) -> Any {
if let number = value as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() {
return number.boolValue
}
return value
}
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
switch (lhs.value, rhs.value) {
case let (l as Int, r as Int): l == r

View File

@@ -0,0 +1,40 @@
import Foundation
import Testing
import OpenClawProtocol
struct AnyCodableTests {
@Test
func encodesNSNumberBooleansAsJSONBooleans() throws {
let trueData = try JSONEncoder().encode(AnyCodable(NSNumber(value: true)))
let falseData = try JSONEncoder().encode(AnyCodable(NSNumber(value: false)))
#expect(String(data: trueData, encoding: .utf8) == "true")
#expect(String(data: falseData, encoding: .utf8) == "false")
}
@Test
func preservesBooleanLiteralsFromJSONSerializationBridge() throws {
let raw = try #require(
JSONSerialization.jsonObject(with: Data(#"{"enabled":true,"nested":{"active":false}}"#.utf8))
as? [String: Any]
)
let enabled = try #require(raw["enabled"])
let nested = try #require(raw["nested"])
struct RequestEnvelope: Codable {
let params: [String: AnyCodable]
}
let envelope = RequestEnvelope(
params: [
"enabled": AnyCodable(enabled),
"nested": AnyCodable(nested),
]
)
let data = try JSONEncoder().encode(envelope)
let json = try #require(String(data: data, encoding: .utf8))
#expect(json.contains(#""enabled":true"#))
#expect(json.contains(#""active":false"#))
}
}