Skip to content

fix(ios): refuse scene-hosted remote content in bridge snapshots - #3319

Merged
thymikee merged 2 commits into
callstack:mainfrom
dhruvkelawala:fix/scene-hosted-remote-content
Oct 8, 2026
Merged

thymikee merged 2 commits into
callstack:mainfrom
dhruvkelawala:fix/scene-hosted-remote-content

Conversation

@dhruvkelawala

@dhruvkelawala dhruvkelawala commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On an iOS 26 simulator, a snapshot taken while a share extension is presented over its host app returns one node:

$ agent-device snapshot -i --session probe     # Photos, with a third-party share extension up
Snapshot: 1 nodes
Hint: sparse accessibility snapshot returned 1 node; ...
@e1 [other] "Photos"

The extension is scene-hosted. Its controls live in the extension's process and reach Photos' tree as one leaf, which the single-process host AX bridge cannot cross. From snapshot --raw (agent-device 0.21.22, iPhone 17 / iOS 26.5):

UIWindow › UITransitionView › UIDropShadowView › UIView ×4 › _UISceneHostingView › _UIScenePresentationView
  › AXRemoteElement  (NSObject, frame 0,0 402×874, no children)

decodeSnapshotBridgeTree already refuses this shape under a WebView (remote-content-boundary, #2484) so the route serves the XCTest runner. ADR 0004 left remote elements under any other host unclassified because "no capture has shown one". This is one.

Change

tree.ts also counts an AXRemoteElement leaf under a _UISceneHostingView as opaque remote content, with the same frame rules as the web case (zero-area or off-screen leaves are published; frameless ones refuse). The refusal, generation circuit and XCTest fallback are unchanged. Doc comments in tree.ts, types.ts, adapter.ts and ADR 0004 now name both hosts.

Verification

  • New tree.test.ts case built from the captured shape: fails without the change and passes with it. packages/platform-apple/src/snapshot-source: 109/109.

  • pnpm check:affected --run: all runnable checks passed. pnpm lint, pnpm typecheck, and format:check pass.

  • pnpm test:unit: 13,007 passed, 1 failed. The failure is scripts/__tests__/apple-ci-impact.test.ts ("a shallow PR merge still yields a known change set…"), which fails the same way on unmodified main on this machine.

  • Live, iOS 26.5 simulator, with the change applied to the 0.21.22 dist. The same screen now reads:

    Simulator AX snapshot unavailable (remote-content-boundary); used XCTest for this app generation.
    @e1 [application] "Photos"
    @e2 [other] "enshrine-0-View"
    @e3 [scroll-area] "LORELEI · SHARE SPIKE" [scrollable]
    @e5 [text] "Give this moment a title."
    @e7 [text-field] "A shared moment" [editable]
    @e8 [button] "Enshrine"
    @e9 [button] "Close"
    

    An @e2e-dev/mobile test that goes Photos → Share → extension → Enshrine, then asserts in the host app, passes using only locators. Before the change it failed at the first extension locator.

Not covered: a physical device (the bridge is simulator-only), Android, and Apple's own scene-hosted services (none captured).

🤖 Generated with Claude Code

View guided diff

On iOS 26 a share or action extension presented over its host app reaches the
host's accessibility tree as one AXRemoteElement leaf under a
_UISceneHostingView; its controls live in the extension's process. The
single-process bridge published that leaf as if the screen were complete, so
a snapshot of Photos with a share extension up returned one `other "Photos"`
node. Count such leaves as opaque remote content, as web-hosted leaves already
are (callstack#2484), so the route serves the XCTest runner, which resolves them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 5 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Comment thread packages/platform-apple/src/snapshot-source/tree.test.ts Outdated
@thymikee

thymikee commented Oct 8, 2026

Copy link
Copy Markdown
Member

The PR is ready at a32d5e0. The change looks correct, CI is green with no failing checks, and there are no conflicts, so nothing is left to do before merge.

Not blocking: the README at apple/snapshot-bridge/README.md still describes remote-content-boundary as WebContent-only (lines 54-57), so it could name the scene-hosting host next to the ADR text, and tree.test.ts could add one assertion that a _UISceneHostingView with ordinary children, or a remote element that has children, decodes with opaqueRemoteElements 0, so the "does not refuse ordinary trees" half is also covered for that host. Take or leave both.

On the open thread from another reviewer: the P3 thread on the scene-host test root class still applies (#3319 (comment)). It is also non-blocking.

I did not run the tests or the live simulator flow. The live result is author-reported against a patched 0.21.22 dist, not the PR head build. I could not check whether other _UISceneHostingView plus AXRemoteElement screens (share sheet, SFSafariViewController, other extensions) are now refused too. Such a screen would take the XCTest fallback, so it would be slower but still correct. The raw capture is not in the PR as a fixture, so the test shape is hand-built from the PR description.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 8, 2026
The scene-hosted test root carried no window, so its viewport was missing and
every positive-area leaf refused through the no-viewport fallback. Shape it as
the bridge captured it: an untyped app root over a UIWindow that reports the
viewport. Add the off-screen, crossed-boundary and ordinary-children cases, and
name the scene-hosting host in the snapshot-bridge README.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dhruvkelawala

Copy link
Copy Markdown
Contributor Author

Thanks @thymikee. I took both suggestions and covered the evidence gap. Everything is in dc3955f.

  • README: apple/snapshot-bridge/README.md "Remote content" now names both hosts.

  • The "does not refuse ordinary trees" half: a _UISceneHostingView with ordinary children, and a scene-hosted remote element that has children, both decode with opaqueRemoteElements 0. The test also now has a reported viewport (cubic's thread, resolved) and an off-screen case.

  • Live run on the PR head build (pnpm build, 0.21.24-dev, unpatched source; iPhone 17 / iOS 26.5): the bridge served Photos and the share-sheet picker with no fallback (23 and 66 visible nodes). It refused once the extension was up:

    Simulator AX snapshot unavailable (remote-content-boundary); used XCTest for this app generation.
    @e1 [application] "Photos"
    @e2 [other] "enshrine-0-View"
    @e3 [scroll-area] "LORELEI · SHARE SPIKE" [scrollable]
    @e4 [other] "LORELEI · SHARE SPIKE"
    @e5 [text] "Give this moment a title."
    @e6 [text] "1 shared Receipt · Legend"
    @e7 [text-field] "A shared moment" [editable]
    @e8 [button] "Enshrine"
    @e9 [button] "Close"
    @e10 [other] "!, Open debugger to view warnings."
    

    @e10 is the React Native dev-build warning badge, not part of the fix.

  • Fixture: what I have is the presented snapshot --raw output, not the bridge's wire envelope. Checking it in as a guest fixture would mean hand-converting it anyway, so the test reproduces the captured ancestry instead (UIWindow › … › _UISceneHostingView › _UIScenePresentationView › AXRemoteElement).

  • Other scene-hosted screens: agreed and still unchecked. The share-sheet picker itself was not refused here (it was served by the bridge before the extension opened), so the refusal followed the extension and not every share flow. Any other host that refuses takes the slower XCTest path but stays correct, as you said.

check:affected --run passes on the new head.

@thymikee

thymikee commented Oct 8, 2026

Copy link
Copy Markdown
Member

I reviewed dc3955f and found no problems in the code. The last review (#3319 (comment)) was also clean at a32d5e0. Since then the tree.test.ts fixture has gained a UIWindow viewport root and asserts viewport.kind 'reported'. The refusal case and the off-screen and zero-area negative cases now run through the reported-viewport branch of isOpaqueRemoteLeaf. The only change in this update touches tree.test.ts and the README, with no production code. The one production change in the PR is still the earlier tree.ts decode.

The one cubic-dev-ai thread on the test fixture is fixed at this head, so please resolve it: #3319 (comment)

I did not run the tests or the live simulator flow. The live result on the PR head is as you reported it. The fixture is hand-built from the PR description and the captured ancestry, and the raw capture is not committed. I also did not check whether other _UISceneHostingView screens, such as SFSafariViewController, now refuse. As you note, a refusal there would only cost the slower XCTest fallback.

Smoke Tests is still running and no check has failed. I expect it to pass, because an ordinary tree has no _UISceneHostingView leaf and so should not hit the new refusal. Once Smoke Tests finishes green, I see nothing else in the way of merge.

@dhruvkelawala

Copy link
Copy Markdown
Contributor Author

Thanks @thymikee. The cubic thread (discussion_r4217451068) was resolved with the dc3955f reply and shows as resolved now. Nothing else is pending on my side.

@thymikee
thymikee merged commit 7b29c35 into callstack:main Oct 8, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants