Skip to content

fix(apple): refuse captures whose window or panel identity is unknown - #3761

Open
thymikee wants to merge 10 commits into
mainfrom
fix/adr-drift-apple-capture-identity
Open

thymikee wants to merge 10 commits into
mainfrom
fix/adr-drift-apple-capture-identity

Conversation

@thymikee

@thymikee thymikee commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Native macOS capture, scroll, and click fallback share one visible, nonempty layer-0 front-window selection and one on-screen window-list query. App-modal level-8 alerts and floating palettes are skipped. Capture refuses instead of substituting another window, with typed exclusion reasons and a runnable snapshot remedy.
  • Foldable capture refuses with capture-panel-unidentified when neither panel power nor primary identity selects a panel. Help warns that an ambiguous primary panel may be dark.

12 files. Part of #3744. ADR 0031 and modal-panel eligibility remain outside this PR.

Validation

  • Head 5e16f816b5: pnpm check:affected --run --head 5e16f816b5 passed (exactCommit=true, checkoutChanged=false) (receipt run-3OxBK9).
  • Swift debug/release builds passed; 15 helper and 6 input tests passed, including level-8 alert exclusion. Focused TypeScript/help/doc checks: 102 passed. XCTest selection passed.
  • Live native TextEdit with the rebuilt helper: 1312×844 PNG shows pr3761-r4-front.txt and its unique marker, with the existing Untitled document behind it. Test document and isolated session pr3761-r4 closed; daemon stopped. State: /private/tmp/agent-device-pr3761-r4.
  • Earlier-head live evidence also covers minimized-window refusal, overlays, diff, and front-only scrolling. Capture exclusions remain fixture-tested; no iOS 27.1 Duo run.
  • New-head CI: pending; Coverage, Integration, native/device lanes remain required.

View guided diff Turn on auto-fix

ADR 0031 decides that a native macOS app screenshot captures only the
app's front window through ScreenCaptureKit (#3188). When the front
window's WindowServer ID was missing from the shareable candidates, the
helper captured the app's largest window instead, so the image could
show a window the user was not looking at.

The helper now selects the front window by ID or refuses with a typed
reason: `window-not-found` when the app has no on-screen window,
`front-window-not-shareable` when ScreenCaptureKit does not offer the
front window. The shared outcomes fixture pins both reasons for Swift
and TypeScript, and the host adds a hint for each.

Evidence: the audit at bd4fd68 (AppWindowScreenshot.swift:21);
re-verified on 4c1c786. Part of #3744.
ADR 0025 decides that only CoreDevice facts identify a panel: the sole
lit panel, otherwise the `primary` panel, and never array position.
When no panel reported `primary` and power did not decide one lit
panel, the inventory captured the first lit or first integrated panel
in list order.

That inventory now names no panel, and capture-target resolution
refuses with COMMAND_FAILED, reason `capture-panel-unidentified`, and a
hint. The sole-lit-panel path and the primary-panel path are unchanged.
The foldable help topic states the refusal.

Evidence: the audit at bd4fd68 (display-inventory.ts:261);
re-verified on 4c1c786. Part of #3744.

@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 10 files

You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-apple/src/os/macos/helper.test.ts Outdated
Comment thread src/commands/system/index.ts Outdated
Comment thread packages/platform-apple/src/os/macos/helper.ts Outdated
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.32 MB 5.33 MB +9.1 kB
Package (unpacked) 5.32 MB 5.33 MB +9.1 kB
Package (download) 1.60 MB 1.60 MB +2.4 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 23.1 ms 23.3 ms +0.2 ms
CLI --help 62.9 ms 62.6 ms -0.3 ms

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3761/

Built to branch gh-pages at 2026-10-17 08:00 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@thymikee

Copy link
Copy Markdown
Member Author

The Swift change looks right, but the native-backend screenshot route has no live evidence yet, so it needs one run on a real host before merge (reviewed at dec7c9b).

AppWindowScreenshot.swift:19 changes the native-backend route for every macOS app capture. When there is no front window ID, or ScreenCaptureKit does not list the front ID, it now throws. It used to return the largest window. If a live app has a CGWindowList front window that SCShareableContent drops (for example a zero-area or transient layer-0 window), every screenshot of that app would now fail. No fixture can show whether that happens, and no live run reached this route because the author's host lacks Screen Recording permission. Could you run it on a host with Screen Recording granted? Set AGENT_DEVICE_MACOS_APP_BACKEND=native and open TextEdit. Run agent-device screenshot and show that the PNG holds the front window. Then minimize every TextEdit window and run screenshot again. It should fail with COMMAND_FAILED, reason window-not-found, and the hint. The minimized case is easy to provoke, unlike front-window-not-shareable. A Duo run is optional, since the changed branch is unit-proven and the lit-panel path is unchanged. Run one only on a host with the iOS 27.1 runtime.

I did not run the Swift or Vitest suites. The regression value of the Swift tests relies on the author's planted-fallback report, because frontCaptureWindow is new. The Duo refusal path is fixture-only, since a CoreDevice payload without a primary panel cannot be produced on a real host.

Smoke Tests failed in the iOS runner suite on RunnerTests.testSynthesizedReplacementLeavesAMaskedFieldUnverified (RunnerTests+SynthesizedTextEntryTests.swift:169), where the masked-field bullet count was 13 instead of 8. This PR changes nothing under apple/runner or the text-entry route, so the failure looks unrelated. No conflicts.

Of the open inline threads, the two docs-wording notes still apply: system/index.ts:281 (lit panel wording) and helper.ts:157-158 (retry hint wording). The helper.test.ts:247 thread does not apply: the PR leaves screenshot argv unchanged, and the existing argv test owns that shape. You can resolve it.

Before merge, this PR needs the live native-backend evidence above: a front-window PNG from on-screen TextEdit, and the window-not-found refusal with the hint when every TextEdit window is minimized.

Choose the first on-screen normal-layer app window with positive alpha and nonempty bounds. Keep interaction window selection unchanged and refuse if an eligible front window is absent from ScreenCaptureKit.

Review: #3761 (comment)
@thymikee

Copy link
Copy Markdown
Member Author

Follow-up head: f8e9688ac6.

  • fa3fe4f774: screenshot selection now rejects off-screen, nonzero-layer, fully transparent, and empty CGWindowList entries before matching the front ID in ScreenCaptureKit. A visible/nonempty front window omitted by ScreenCaptureKit still refuses; there is no substitution.
  • a77578f8ee, f8e9688ac6: capture-exclusion hint and ambiguous-primary-panel wording fixed.
  • Threads: 2 fixed, 1 not applicable and resolved per maintainer; 0 rejected/open.

pnpm check:affected --run --head f8e9688ac6 passed (exactCommit=true, receipt run-KhgbXp). Focused Vitest: 48 passed. Swift helper: 13 passed; release build and XCTest selection passed. Removing alpha/size filters in a disposable checkout made both added tests fail (11 passed); cleanup completed.

Previous-head CI: only iOS Smoke Tests failed, at the unchanged masked-field assertion (13 bullets vs 8). PR #3722 has the identical failure. That runner XCTest exercises neither changed capture route. Android/Linux/macOS smoke, Coverage, Integration, package/guards, docs/preview, CodeQL, Size, and cubic passed; the fixture-cache matrix job was skipped. New-head CI is pending.

Live check remains with maintainer: preflight is false; native TextEdit screenshot returned COMMAND_FAILED, permission: screen-recording, before dispatch. With permission granted, verify the front-window PNG and minimized-all-windows window-not-found plus hint. Isolated daemon stopped.

@thymikee

Copy link
Copy Markdown
Member Author

Live macOS evidence at f8e9688ac6 (native backend, Screen Recording granted, CGPreflightScreenCaptureAccess() == true).

Setup: AGENT_DEVICE_MACOS_APP_BACKEND=native, isolated --state-dir, TextEdit with three windows: live3761-front.txt (front), live3761-back.txt, and an existing Untitled. The helper that ran (~/.agent-device/macos-helper/current/agent-device-macos-helper, sha1 ce2f5974…) is byte-identical to pnpm build:macos-helper from this head.

  1. screenshot front.png → 1312×844 PNG of live3761-front.txt (title bar and text "FRONT WINDOW" visible), not another window.
  2. All three windows minimized through their AX minimize buttons, then screenshot → COMMAND_FAILED, details.reason: "window-not-found", hint "Open or unminimize a window of the app on the current Space, then retry the screenshot.", dispatched: "no". No PNG was written.
  3. One window restored: screenshot --overlay-refs annotates 4 refs on the front window; diff screenshot --baseline front.png reports 100 of 1,107,328 pixels changed, one 4×26 region at the text caret (the blinking cursor). Overlay and live diff use the front window.

Not run: a sheet or popover, a fullscreen Space, and the iOS 27.1 iPhone Duo (this host has no iOS 27.1 runtime). Cleanup: test documents closed, Untitled restored, session closed, daemon stopped.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed all three follow-up findings in separate commits: M1 dd5a35e488 shares front-window eligibility; L1 c326cafbfd defines it in help/docs; L2 0de4a1e2c8 distinguishes capture exclusions with a typed reason and non-retry hint. M2/ADR 0031 remain out of scope. No unresolved review threads.

At 0de4a1e2c8: 14 Swift tests passed, focused TypeScript/help/doc checks passed, CLI/native builds and XCTest selection passed. check:affected --run --head 0de4a1e2c8 passed with exactCommit=true, checkoutChanged=false (receipt run-CDMHCO). Disposable mutations fail the intended selection/exclusion tests; restoring fixes passes.

Live on this Mac with AGENT_DEVICE_MACOS_APP_BACKEND=native, worktree-built helper, session pr3761-r2, and worktree-local state: the 1312×844 PNG shows pr3761-r2-front.txt, not the back document. scroll down --pixels 300 reports ax-scroll-bar; the next PNG starts at FRONT line 23. AX text y changes 221→−80 only in the front document; back stays 192. Both created documents closed through close buttons; existing Untitled untouched; session/daemon stopped.

Prior-head CI is green; new-head CI is pending.

@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 8 files (changes from recent commits).

You’re at about 96% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Share the on-screen window-list query while preserving nil versus empty point refusals. Pin layer-8 alert exclusion at the shared selection boundary.

Review: #3761 (comment)
@thymikee

Copy link
Copy Markdown
Member Author

Round 4 head: 5e16f816b5.

  • bb69e6a301: one on-screen query and shared frontWindow owner; nil-versus-empty point refusals preserved; level-8 alert fixture added.
  • 5e16f816b5: help/docs state layer-0 eligibility and skipped app-modal alerts; capture-exclusion hint gives runnable snapshot step.
  • Threads: 1 fixed and resolved (cubic); 0 rejected/open. All four round-3 low findings addressed. Issue comments, reviews, and inline threads reread.

Validation: exact-head affected gate passed (exactCommit=true, checkoutChanged=false, receipt run-3OxBK9); format, Swift debug/release builds, 15 helper + 6 input tests, 102 focused TS/help/doc tests, XCTest selection passed. Initial gate failed an unchanged config assertion because generated PID path test-run-12316 matched synthetic run-123; controlled reproduction fails identically on base 0de4a1e2c8 and head (1 failed/18 passed), then normal gate passed. Experiment checkout removed.

Live native TextEdit, rebuilt helper: 1312×844 PNG visibly captures pr3761-r4-front.txt, with Untitled behind it. Test document/session closed; isolated daemon stopped.

New-head CI pending; monitoring for up to ~40 minutes. No merge or approval.

@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.

1 issue found across 7 files (changes from recent commits).

You’re at about 96% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift">

<violation number="1" location="apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift:515">
P3: The `?? []` default turns a failed window-list read into "no front window", so screenshot and scroll report `window-not-found`. That tells the caller to open a window, which is wrong when the read itself failed. `requireAppWindowPoint` already keeps this case distinct, so the two paths now disagree. Pass the optional through so an unreadable list is refused with its own reason, or document the collapse if it is intended.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

as? [[String: Any]]
}

func frontWindow(pid: pid_t, windowInfo: [[String: Any]] = appWindowInfo() ?? []) -> OnScreenWindow? {

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.

P3: The ?? [] default turns a failed window-list read into "no front window", so screenshot and scroll report window-not-found. That tells the caller to open a window, which is wrong when the read itself failed. requireAppWindowPoint already keeps this case distinct, so the two paths now disagree. Pass the optional through so an unreadable list is refused with its own reason, or document the collapse if it is intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift, line 515:

<comment>The `?? []` default turns a failed window-list read into "no front window", so screenshot and scroll report `window-not-found`. That tells the caller to open a window, which is wrong when the read itself failed. `requireAppWindowPoint` already keeps this case distinct, so the two paths now disagree. Pass the optional through so an unreadable list is refused with its own reason, or document the collapse if it is intended.</comment>

<file context>
@@ -509,13 +507,17 @@ func onScreenWindows(pid: pid_t, windowInfo: [[String: Any]]) -> [OnScreenWindow
+    as? [[String: Any]]
+}
+
+func frontWindow(pid: pid_t, windowInfo: [[String: Any]] = appWindowInfo() ?? []) -> OnScreenWindow? {
+  onScreenWindows(pid: pid, windowInfo: windowInfo).first
 }
</file context>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant