Skip to content

fix(macos): use foundationValue when serializing browser proxy POST body#41635

Closed
Effet wants to merge 103 commits intoopenclaw:mainfrom
Effet:fix/macos-browser-proxy-post-body-crash
Closed

fix(macos): use foundationValue when serializing browser proxy POST body#41635
Effet wants to merge 103 commits intoopenclaw:mainfrom
Effet:fix/macos-browser-proxy-post-body-crash

Conversation

@Effet
Copy link
Copy Markdown
Contributor

@Effet Effet commented Mar 10, 2026

Summary

  • Problem: MacNodeBrowserProxy.makeRequest passes params.body?.value directly to JSONSerialization.data(withJSONObject:). AnyCodable.init(from:) stores decoded JSON objects as [String: AnyCodable] internally — a pure Swift struct (__SwiftValue) that NSJSONSerialization can't handle, causing an NSInvalidArgumentException crash (SIGABRT) on every non-GET request with a JSON body.
  • Why it matters: Any POST/PUT/PATCH request routed through the Mac browser proxy crashes the app. The crash is deterministic and not edge-case.
  • What changed: Use params.body?.foundationValue instead of params.body?.value. foundationValue (already defined in AnyCodable+Helpers.swift) recursively converts [String: AnyCodable][String: Any] so JSONSerialization receives only Foundation-compatible types.
  • What did NOT change: No behavior change for GET requests or response parsing; no new dependencies.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • UI / DX

Linked Issue/PR

User-visible / Behavior Changes

Mac app no longer crashes when making POST/PUT/PATCH requests through the browser proxy.

Security Impact (required)

  • 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

Repro + Verification

Environment

  • OS: macOS (latest)
  • Runtime: Mac app (MacNodeRuntime)

Steps

  1. Open Mac app with gateway running
  2. Trigger any browser proxy call that uses a non-GET method with a JSON body (e.g. a POST via MacNodeBrowserProxy.request)
  3. App crashes with NSInvalidArgumentException: Invalid type in JSON write (__SwiftValue)

Expected

  • Request serializes and sends successfully

Actual

  • SIGABRT crash in MacNodeBrowserProxy.makeRequest at JSONSerialization.data(withJSONObject:)

Evidence

  • Regression test added: postRequestSerializesNestedBodyWithoutCrash in MacNodeBrowserProxyTests.swift — covers nested dict + array body, was impossible to add before the fix (would crash)

Human Verification (required)

  • Verified scenarios: read crash stack trace, identified root cause in source, confirmed foundationValue is the correct converter, added failing→passing test
  • Edge cases checked: nested objects, arrays, null values all handled by existing foundationValue recursion
  • What you did not verify: live end-to-end POST request through running gateway (no Mac build in this session)

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)

  • Revert: restore params.body?.value in MacNodeBrowserProxy.swift:149
  • Known bad symptom: app crashes on any browser proxy POST request (same as before fix)

Risks and Mitigations

  • Risk: foundationValue on an unrecognized type falls through to self.value (the default case) — same behavior as before, so no regression risk.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR fixes a deterministic SIGABRT crash in MacNodeBrowserProxy.makeRequest when making any non-GET HTTP request with a JSON body. The root cause was passing AnyCodable.value directly to JSONSerialization: since AnyCodable.init(from:) stores decoded JSON objects as [String: AnyCodable] (a pure Swift struct), NSJSONSerialization would throw NSInvalidArgumentException: Invalid type in JSON write (__SwiftValue) on every such call. The fix correctly substitutes body.foundationValue, which already exists in AnyCodable+Helpers.swift and recursively converts [String: AnyCodable][String: Any] so only Foundation-compatible types reach the serializer. A targeted regression test is included.

  • Fix (MacNodeBrowserProxy.swift line 149–150): params.body?.value replaced with params.body + body.foundationValue; semantics of when the body is set are unchanged (both paths skip the body iff params.body is nil)
  • foundationValue: Handles all five decoded cases from AnyCodable.init(from:) — dicts, arrays, and primitives/null — recursively, with the default branch returning self.value (same as before), so no regression for unrecognized types
  • Regression test: Covers nested dict + array bodies; previously would crash before the fix; validates round-tripped values after serialization/deserialization

Confidence Score: 4/5

  • Safe to merge — fixes a deterministic crash with a minimal, well-understood one-line change backed by a meaningful regression test.
  • This PR is safe to merge. The fix is correct: foundationValue recursively converts [String: AnyCodable] to [String: Any], and the default branch preserves prior behavior for primitives/null. The change in optional binding semantics (params.body?.valueparams.body + .foundationValue) is equivalent — both paths skip body attachment iff params.body is nil. The regression test is meaningful and would catch a recurrence. Only live end-to-end POST testing is missing, which the author explicitly acknowledges.
  • No files require special attention.

Last reviewed commit: 991d42b

@Effet Effet force-pushed the fix/macos-browser-proxy-post-body-crash branch from 991d42b to b33b805 Compare March 10, 2026 05:25
@frankekn frankekn self-assigned this Mar 11, 2026
urianpaul94 and others added 24 commits March 11, 2026 17:04
…dary marker variants (openclaw#35983)

Merged via squash.

Prepared head SHA: ff07dc4
Co-authored-by: urianpaul94 <[email protected]>
Co-authored-by: frankekn <[email protected]>
Reviewed-by: @frankekn
…penclaw#40740)

Merged via squash.

Prepared head SHA: a4456d4
Co-authored-by: sircrumpet <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
Merged via squash.

Prepared head SHA: 0a8ebf8
Co-authored-by: frankekn <[email protected]>
Co-authored-by: frankekn <[email protected]>
Reviewed-by: @frankekn
Merged via squash.

Prepared head SHA: 8abc6c1
Co-authored-by: BunsDev <[email protected]>
Co-authored-by: BunsDev <[email protected]>
Reviewed-by: @BunsDev
…penclaw#14967)

Merged via squash.

Prepared head SHA: 1bb49b2
Co-authored-by: rixau <[email protected]>
Co-authored-by: BunsDev <[email protected]>
Reviewed-by: @BunsDev
…ess is ro (openclaw#40757)

Merged via squash.

Prepared head SHA: 0e8b27b
Co-authored-by: dsantoreis <[email protected]>
Co-authored-by: mcaxtr <[email protected]>
Reviewed-by: @mcaxtr
…claw#41176)

Merged via squash.

Prepared head SHA: 33cac4c
Co-authored-by: hnykda <[email protected]>
Co-authored-by: mukhtharcm <[email protected]>
Reviewed-by: @mukhtharcm
… Framework compatibility (openclaw#41838)

* fix(msteams): use General channel conversation ID as team key for Bot Framework compatibility

Bot Framework sends `activity.channelData.team.id` as the General channel's
conversation ID (e.g. `19:[email protected]`), not the Graph API group GUID
(e.g. `fa101332-cf00-431b-b0ea-f701a85fde81`). The startup resolver was
storing the Graph GUID as the team config key, so runtime matching always
failed and every channel message was silently dropped.

Fix: always call `listChannelsForTeam` during resolution to find the General
channel, then use its conversation ID as the stored `teamId`. When a specific
channel is also configured, reuse the same channel list rather than issuing a
second API call. Falls back to the Graph GUID if the General channel cannot
be found (renamed/deleted edge case).

Fixes openclaw#41390

* fix(msteams): handle listChannelsForTeam failure gracefully

* fix(msteams): trim General channel ID and guard against empty string

* fix: document MS Teams allowlist team-key fix (openclaw#41838) (thanks @BradGroux)

---------

Co-authored-by: bradgroux <[email protected]>
Co-authored-by: Onur <[email protected]>
Merged via squash.

Prepared head SHA: 5cffcb0
Co-authored-by: teconomix <[email protected]>
Co-authored-by: mukhtharcm <[email protected]>
Reviewed-by: @mukhtharcm
* ACPX: bump bundled acpx to 0.1.16

* fix: bump acpx pin to 0.1.16 (openclaw#41975) (thanks @dutifulbob)

---------

Co-authored-by: Onur <[email protected]>
…me (openclaw#41847)

* feat(acp): add resumeSessionId to sessions_spawn for ACP session resume

Thread resumeSessionId through the ACP session spawn pipeline so agents
can resume existing sessions (e.g. a prior Codex conversation) instead
of starting fresh.

Flow: sessions_spawn tool → spawnAcpDirect → initializeSession →
ensureSession → acpx --resume-session flag → agent session/load

- Add resumeSessionId param to sessions-spawn-tool schema with
  description so agents can discover and use it
- Thread through SpawnAcpParams → AcpInitializeSessionInput →
  AcpRuntimeEnsureInput → acpx extension runtime
- Pass as --resume-session flag to acpx CLI
- Error hard (exit 4) on non-existent session, no silent fallback
- All new fields optional for backward compatibility

Depends on acpx >= 0.1.16 (openclaw/acpx#85, merged, pending release).

Tests: 26/26 pass (runtime + tool schema)
Verified e2e: Discord → sessions_spawn(resumeSessionId) → Codex
resumed session and recalled stored secret.

🤖 AI-assisted

* fix: guard resumeSessionId against non-ACP runtime

Add early-return error when resumeSessionId is passed without
runtime="acp" (mirrors existing streamTo guard). Without this,
the parameter is silently ignored and the agent gets a fresh
session instead of resuming.

Also update schema description to note the runtime=acp requirement.

Addresses Greptile review feedback.

* ACP: add changelog entry for session resume (openclaw#41847) (thanks @pejmanjohn)

---------

Co-authored-by: Pejman Pour-Moezzi <[email protected]>
Co-authored-by: Onur <[email protected]>
…claw#18655)

Merged via squash.

Prepared head SHA: d30fff1
Co-authored-by: echo931 <[email protected]>
Co-authored-by: mukhtharcm <[email protected]>
Reviewed-by: @mukhtharcm
…k runs (openclaw#41711)

Merged via squash.

Prepared head SHA: 8be8967
Co-authored-by: cgdusek <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf
Merged via squash.

Prepared head SHA: 85c4eb7
Co-authored-by: smysle <[email protected]>
Co-authored-by: hydro13 <[email protected]>
Reviewed-by: @hydro13
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
…34369)

Strip trailing /v1beta from baseUrl before appending the version
segment, so callers that already include /v1beta in their base URL
(e.g. subagent-registry) no longer produce /v1beta/v1beta/models/…
which results in a 404 from the Gemini API.

Closes openclaw#34312

Co-authored-by: Claude Opus 4.6 <[email protected]>
* docs(acp): document resumeSessionId for session resume

* docs: clarify ACP resumeSessionId thread/mode behavior (openclaw#42280) (thanks @pejmanjohn)

---------

Co-authored-by: Onur <[email protected]>
…penclaw#42278)

Merged via squash.

Prepared head SHA: f3cdfa7
Co-authored-by: CryUshio <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

1 similar comment
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

5 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

15 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

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: 3874cf00f1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +159 to +163
if (!child.stdin) {
const identity = await runPinnedWriteFallback(params);
await exitPromise.catch(() => {});
return identity;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fall back when pinned-write helper startup fails

The fallback path is guarded only by if (!child.stdin), but with stdio: ["pipe", "pipe", "pipe"] that branch is effectively unreachable for normal process creation, so a missing python3 binary causes the helper call to fail instead of degrading to runPinnedWriteFallback. On non-Windows hosts without Python installed, every safe write/copy now fails hard, which is a runtime regression for core file mutation paths.

Useful? React with 👍 / 👎.

" chunk = sys.stdin.buffer.read(65536)",
" if not chunk:",
" break",
" os.write(temp_fd, chunk)",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Loop until full chunk is written in pinned write script

The embedded Python writer calls os.write(temp_fd, chunk) once per chunk and ignores the returned byte count. os.write may legally short-write under resource pressure or interruption, so this can silently persist truncated content while still renaming the temp file into place and reporting success, leading to data corruption in writeFileWithinRoot/copyFileWithinRoot paths.

Useful? React with 👍 / 👎.

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

Labels

agents Agent runtime and tooling app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: acpx extensions: memory-core Extension: memory-core gateway Gateway runtime scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS app crashes on browser.proxy invoke with Invalid type in JSON write (__SwiftValue)