Skip to content

fix(mac): guard browser.proxy JSON serialization against non-serializable values#45895

Open
vernonstinebaker wants to merge 3 commits intoopenclaw:mainfrom
vernonstinebaker:fix/browser-proxy-json-serialization
Open

fix(mac): guard browser.proxy JSON serialization against non-serializable values#45895
vernonstinebaker wants to merge 3 commits intoopenclaw:mainfrom
vernonstinebaker:fix/browser-proxy-json-serialization

Conversation

@vernonstinebaker
Copy link
Copy Markdown

Summary

  • Problem: The macOS menubar app crashes with SIGABRT when MacNodeBrowserProxy.makeRequest passes non-JSON-serializable values to NSJSONSerialization. Crash reports show __SwiftValue exceptions in the serialization path.
  • Why it matters: The app crashes and disconnects from the gateway whenever a browser.proxy request contains unexpected value types, breaking browser control for users. Four duplicate issues confirm this is actively hitting users.
  • What changed: Added sanitizeForJSON(_:) recursive sanitizer in MacNodeBrowserProxy that converts non-JSON-serializable values to String(describing:) before passing to NSJSONSerialization. Also added explicit CronJob memberwise init to fix a pre-existing Swift 6.2 build error in the test target.
  • What did NOT change: No changes to the gateway, protocol, or network layer. No changes to AnyCodable decoding — the sanitizer is purely a defensive layer at serialization time.

Change Type

  • Bug fix

Scope

  • Skills / tool execution

Linked Issue/PR

User-visible / Behavior Changes

None. The app previously crashed silently; it now gracefully sanitizes the request body.

Security Impact

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)

Evidence

  • Failing test/log before + passing after
    • Crash log: OpenClaw-2026-03-13-041109.ips (SIGABRT in MacNodeBrowserProxy.makeRequest, lastExceptionBacktrace at NSJSONSerialization dataWithJSONObject)
    • Gateway log: node.invoke returns UNAVAILABLE: Error: node disconnected (browser.proxy) at the same timestamp
  • All 7 tests pass (5 new + 2 existing):
✔ sanitizeForJSONConvertsNonSerializableValuesToStrings
✔ sanitizeForJSONRecursesIntoDictionaries
✔ sanitizeForJSONRecursesIntoArrays
✔ sanitizeForJSONPreservesJSONSafeValues
✔ postRequestWithNonSerializableBodyDoesNotCrash
✔ postRequestSerializesNestedBodyWithoutCrash (existing)
✔ request uses browser control endpoint and wraps result (existing)

Human Verification (required)

  • Verified scenarios: sanitizeForJSON with opaque Swift structs, nested dictionaries, arrays, JSON-safe primitives (String/Int/Bool/NSNull), full proxy request round-trip through mock HTTP
  • Edge cases checked: NSNull passthrough, NSNumber boolean detection via CFGetTypeID, recursive nesting through mixed dict/array structures
  • What you did not verify: Runtime behavior with the signed menubar app binary (requires full app build + signing to test with live gateway)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert commit d93287c
  • Files/config to restore: apps/macos/Sources/OpenClaw/NodeMode/MacNodeBrowserProxy.swift
  • Known bad symptoms reviewers should watch for: browser.proxy requests returning garbled body content instead of JSON

Risks and Mitigations

  • Risk: String(describing:) may produce verbose or unexpected output for complex opaque types
    • Mitigation: This is a last-resort fallback for edge cases that previously caused hard crashes. Normal JSON payloads (the common case) pass through untouched by the fast-path isValidJSONObject check.

🤖 AI-assisted. Fully tested (7/7 tests passing).

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR fixes a SIGABRT crash in the macOS menubar app where MacNodeBrowserProxy.makeRequest passed non-JSON-serializable __SwiftValue types from AnyCodable.foundationValue directly to NSJSONSerialization, causing hard crashes on affected users. The fix introduces a recursive sanitizeForJSON(_:) sanitizer that replaces any unserializable value with its String(describing:) representation before serialization, while valid JSON payloads (the common path) are returned unchanged via a JSONSerialization.isValidJSONObject fast-path check.

Changes:

  • MacNodeBrowserProxy.swift: Core fix — sanitizeForJSON(_:) defensive sanitizer called before JSONSerialization.data(withJSONObject:) on POST bodies
  • CronModels.swift: Adds an explicit CronJob memberwise initializer to fix a pre-existing Swift 6.2 build error in the test target; correctly assigns sessionTargetRaw = sessionTarget.rawValue for the backing private storage
  • CronSettings+Testing.swift: Adds explicit nil as Type? annotations at two call sites to resolve type-inference ambiguity introduced by the new initializer
  • MacNodeBrowserProxyTests.swift: 5 new tests covering the sanitizer at unit level (non-serializable values, dict/array recursion, JSON-safe passthrough) and one integration-level regression test; note the integration test postRequestWithNonSerializableBodyDoesNotCrash uses a valid JSON body so it doesn't actually exercise the crash path end-to-end
  • Package.resolved: Includes an unrelated ElevenLabsKit revision bump at the same semver tag 0.1.0 — this should be confirmed as intentional

Confidence Score: 4/5

  • Safe to merge — the core sanitization logic is correct and the fix is narrowly scoped to the crash path, with no protocol or network layer changes
  • The sanitizeForJSON implementation is logically sound: the isValidJSONObject([value]) fast-path correctly short-circuits for the common case (including entire valid subtrees), and the recursive switch correctly handles all edge cases. The CronJob init and nil-type disambiguation changes are straightforward correctness fixes. Score is 4 rather than 5 due to: (1) the integration regression test not actually exercising the non-serializable crash path, leaving a gap in end-to-end coverage; and (2) the unrelated ElevenLabsKit revision bump at the same semver version, which is an unexplained incidental diff.
  • apps/macos/Package.resolved — the unrelated ElevenLabsKit revision bump should be confirmed as intentional before merging

Comments Outside Diff (1)

  1. apps/macos/Package.resolved, line 24-31 (link)

    Unrelated ElevenLabsKit revision bump

    The ElevenLabsKit dependency revision changed (c8679fbd7e3c948d) while staying at 0.1.0. This appears unrelated to the proxy JSON-serialization fix. A revision change at the same semver tag can indicate a force-pushed tag upstream, which is generally considered unsafe (the content at that revision is not guaranteed stable).

    Please confirm this bump is intentional and, if so, document why it's included in this PR. If it snuck in from a rebase or local branch state, it should be reverted to keep the diff focused.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/macos/Package.resolved
    Line: 24-31
    
    Comment:
    **Unrelated `ElevenLabsKit` revision bump**
    
    The `ElevenLabsKit` dependency revision changed (`c8679fbd``7e3c948d`) while staying at `0.1.0`. This appears unrelated to the proxy JSON-serialization fix. A revision change at the same semver tag can indicate a force-pushed tag upstream, which is generally considered unsafe (the content at that revision is not guaranteed stable).
    
    Please confirm this bump is intentional and, if so, document why it's included in this PR. If it snuck in from a rebase or local branch state, it should be reverted to keep the diff focused.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/macos/Tests/OpenClawIPCTests/MacNodeBrowserProxyTests.swift
Line: 145-181

Comment:
**Integration test doesn't exercise the non-serializable path**

The test name `postRequestWithNonSerializableBodyDoesNotCrash` implies it verifies the crash fix, but the body `{"key":"val"}` is perfectly valid JSON and passes through `sanitizeForJSON` via the `isValidJSONObject` fast-path without ever touching the fallback serialization logic. The comment even acknowledges this: "The sanitization layer ensures no crash even if the gateway sends something unexpected in the future."

This test is essentially a duplicate of the existing `postRequestSerializesNestedBodyWithoutCrash` with a simpler body. The real crash scenario (an opaque `__SwiftValue` reaching `NSJSONSerialization`) is only exercised at unit level in `sanitizeForJSONConvertsNonSerializableValuesToStrings`. Consider either renaming this test to reflect what it actually covers (e.g. `postRequestWithSimpleBodySerializesCorrectly`), or extending it to inject a non-serializable value directly into the `sanitizeForJSON``makeRequest` path so the regression is tested end-to-end.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/macos/Package.resolved
Line: 24-31

Comment:
**Unrelated `ElevenLabsKit` revision bump**

The `ElevenLabsKit` dependency revision changed (`c8679fbd``7e3c948d`) while staying at `0.1.0`. This appears unrelated to the proxy JSON-serialization fix. A revision change at the same semver tag can indicate a force-pushed tag upstream, which is generally considered unsafe (the content at that revision is not guaranteed stable).

Please confirm this bump is intentional and, if so, document why it's included in this PR. If it snuck in from a rebase or local branch state, it should be reverted to keep the diff focused.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: d93287c

Comment on lines +145 to +181
@Test func postRequestWithNonSerializableBodyDoesNotCrash() async throws {
actor BodyCapture {
private var body: Data?
func set(_ body: Data?) { self.body = body }
func get() -> Data? { self.body }
}

let capturedBody = BodyCapture()
let proxy = MacNodeBrowserProxy(
endpointProvider: {
MacNodeBrowserProxy.Endpoint(
baseURL: URL(string: "http://127.0.0.1:18791")!,
token: nil,
password: nil)
},
performRequest: { request in
await capturedBody.set(request.httpBody)
let url = try #require(request.url)
let response = try #require(
HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: nil))
return (Data(#"{"ok":true}"#.utf8), response)
})

// Encode a body that, after AnyCodable decoding, is fine.
// The sanitization layer ensures no crash even if the gateway
// sends something unexpected in the future.
_ = try await proxy.request(
paramsJSON: #"{"method":"POST","path":"/action","body":{"key":"val"}}"#)

let bodyData = try #require(await capturedBody.get())
let parsed = try #require(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
#expect(parsed["key"] as? String == "val")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Integration test doesn't exercise the non-serializable path

The test name postRequestWithNonSerializableBodyDoesNotCrash implies it verifies the crash fix, but the body {"key":"val"} is perfectly valid JSON and passes through sanitizeForJSON via the isValidJSONObject fast-path without ever touching the fallback serialization logic. The comment even acknowledges this: "The sanitization layer ensures no crash even if the gateway sends something unexpected in the future."

This test is essentially a duplicate of the existing postRequestSerializesNestedBodyWithoutCrash with a simpler body. The real crash scenario (an opaque __SwiftValue reaching NSJSONSerialization) is only exercised at unit level in sanitizeForJSONConvertsNonSerializableValuesToStrings. Consider either renaming this test to reflect what it actually covers (e.g. postRequestWithSimpleBodySerializesCorrectly), or extending it to inject a non-serializable value directly into the sanitizeForJSONmakeRequest path so the regression is tested end-to-end.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/macos/Tests/OpenClawIPCTests/MacNodeBrowserProxyTests.swift
Line: 145-181

Comment:
**Integration test doesn't exercise the non-serializable path**

The test name `postRequestWithNonSerializableBodyDoesNotCrash` implies it verifies the crash fix, but the body `{"key":"val"}` is perfectly valid JSON and passes through `sanitizeForJSON` via the `isValidJSONObject` fast-path without ever touching the fallback serialization logic. The comment even acknowledges this: "The sanitization layer ensures no crash even if the gateway sends something unexpected in the future."

This test is essentially a duplicate of the existing `postRequestSerializesNestedBodyWithoutCrash` with a simpler body. The real crash scenario (an opaque `__SwiftValue` reaching `NSJSONSerialization`) is only exercised at unit level in `sanitizeForJSONConvertsNonSerializableValuesToStrings`. Consider either renaming this test to reflect what it actually covers (e.g. `postRequestWithSimpleBodySerializesCorrectly`), or extending it to inject a non-serializable value directly into the `sanitizeForJSON``makeRequest` path so the regression is tested end-to-end.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d93287ca33

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revert unintended ElevenLabsKit lockfile revision bump

This change updates elevenlabskit to a different git revision while keeping the same 0.1.0 version, and there is no matching manifest or code change that explains a dependency upgrade. That silently swaps the source code pulled for macOS builds under the same semantic version, which weakens reproducibility and can introduce unreviewed behavior changes in production artifacts; if this was not a deliberate dependency update, the lockfile change should be dropped from this bugfix commit.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants