macOS backend: full non-background parity, builds on macos-latest, awaits on-device verification - #3
Draft
NORTHTEKDevs wants to merge 13 commits into
Draft
macOS backend: full non-background parity, builds on macos-latest, awaits on-device verification#3NORTHTEKDevs wants to merge 13 commits into
NORTHTEKDevs wants to merge 13 commits into
Conversation
Ghost runs on Windows only today, but the workspace didn't say so at build time: seven crates pulled the `windows` crate unconditionally (directly or transitively), so `cargo build` on macOS/Linux produced hundreds of Win32 FFI errors rather than a clear answer, and CI had no non-Windows target at all. Build honesty: - Move `windows`/`windows-core` into `[target.'cfg(windows)'.dependencies]` in ghost-core, ghost-cache, ghost-session, ghost-cli, ghost-mcp. - Add `#![cfg(windows)]` plus a six-line build-script guard to all seven Windows-locked crates (the five above plus ghost-intent and ghost-http, which inherit the lock through ghost-core/ghost-session). Off Windows each now fails with exactly one sentence pointing at docs/cross-platform.md. - Add `default-members = [ghost-platform, ghost-ground]` so a bare `cargo build` is green and honest on macOS/Linux; `.cargo/config.toml` adds build-all/check-all/test-all aliases for Windows development. - CI gains check-macos and check-linux jobs running `cargo check -p ghost-platform -p ghost-ground` and `cargo test -p ghost-platform` on their native hosts. Existing Windows jobs and `-D warnings` are untouched. - New ghost-platform integration test asserts the host backend reports functional only on Windows — a tripwire against an accidental capability flip. Surface honesty (per docs/audits/2026-07-honesty-audit.md, items S1-a, S1-c, S1-d, S2-a, S3-a, S3-b): README leads with "Windows 10/11 only" and demotes mac/linux to roadmap, Install header carries the OS requirement, the MCP tool count reads 20 verbs to match the crate description and CHANGELOG, the architecture diagram shows ghost-platform, comparison.md gains a platform coverage row, and server.json catches up to 0.16.0. No Windows source semantics change. No capability flag changes: macOS and Linux still report functional: false and stay that way until the on-device checklist in docs/plans/2026-07-cross-platform-plan.md passes.
…scope The Release build job broke when `default-members` was narrowed to the portable crates: `cargo build --bin ghost` no longer sees ghost-cli's target. Name the packages explicitly. Adds a `Build (macOS)` job on macos-latest. This is the authoritative proof that Ghost's macOS code compiles and links against a real Apple SDK — local cross-compilation is only a convenience. Currently green on empty scaffolding; the backend lands on top of it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ional=false) Replaces the inert macos.rs scaffold with a real backend: element discovery and act/read-back over AXUIElement, keyboard and mouse synthesis over CGEvent, window enumeration and focus over CGWindowList + NSWorkspace, capture over CGWindowListCreateImage, and clipboard over NSPasteboard. capabilities_for(Platform::MacOS).functional stays false. The new MAC_FEATURES list says which features have native code, which is a different claim from saying they work; nothing here has been executed on a Mac. The flag flips when `ghost doctor --mac` passes on real hardware. Notable choices, all made to reduce the number of ways this can fail on the one Mac that will ever run it before it ships: - No private symbols. `_AXUIElementGetWindow` would be the easy way to map an AXUIElement to a CGWindowID, but an undocumented symbol that fails to link would fail on the partner's machine and not in CI, since an rlib does not link. Ghost correlates the two worlds through pid + window title instead. - CGWindowListCreateImage over ScreenCaptureKit: deprecated but synchronous C, versus an async block-based ObjC API. - Retina scale is measured per capture as image_pixels / requested_points rather than queried from the display, so a coordinate derived from an image is converted by the factor that actually applied to that image. - Hotkey modifiers are CGEventFlags on the key event, never separate synthetic keydown/keyup, which can leave a modifier stuck down system-wide. - Every AXError variant is named in the match; there is no `_` catch-all, and an out-of-contract value is preserved as Unknown(i32) for bug reports. The tripwire test's macOS arm was made more precise rather than weaker: it still asserts functional=false and BackgroundDispatch unclaimed off Windows, and Linux still must advertise no Feature at all. Cargo.lock grew by the mac-only dependency tree; no existing package changed version.
chunk_utf16 could not make progress when a surrogate pair began exactly at a chunk boundary and the chunk size was one: shrinking to avoid splitting the pair produced a zero-length chunk, so the loop pushed empty vectors until the process was OOM-killed. It hung CI instead of failing it. A chunk may now overshoot the target by exactly one unit, which is the only way to keep a pair intact. find() rejected Description locators only after resolving the window, so on a runner with no windows the caller was told the window was missing rather than that the locator is ghost-ground's job. The check now runs first, and find_ax no longer treats a Description as simply "no match". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The on-device smoke test has to launch TextEdit, walk down to a File > New menu item, and prove a screenshot is not blank. None of those were reachable through the backend yet. - window: launch_app/running_app_pid/wait_for_app/wait_for_window_count, so a caller can wait for an asynchronous launch instead of guessing at a sleep. - ax: menu_bar (only reachable from the application element, never a window), plus bounded breadth-first finders by name and by raw AX role. - capture: precompute Capture::blank while the RGBA buffer is still in hand. A missing Screen Recording grant yields a valid all-black image rather than an error, so "not blank" is the only way to tell the difference — and after PNG encoding the pixels are gone. - error: a Timeout variant, distinct because nothing failed; the OS was simply still working. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step B — wire the backend in without disturbing the Windows engine: - backend.rs holds `SessionBackend`, extracted so both hosts implement one trait. It is `#[async_trait(?Send)]` because COM forces it: GhostSession holds UIA interface pointers with thread affinity, so requiring Send would leave the Windows engine unable to implement its own trait. - win_backend.rs adapts the existing GhostSession to that trait. No Windows code was moved or deleted; it is a delegation layer. - `Session` is a new enum, deliberately NOT a type alias for GhostSession. GhostSession exposes ~60 Windows-only methods (intent execution, OCR, vision, background dispatch); aliasing would force a macOS arm for each that could only return "unsupported", and a method that exists and always fails is worse than one that does not exist, because only the second is caught by the compiler. - ghost-session and ghost-cli lose the crate-level `#![cfg(windows)]` and build.rs from the previous PR, gated now to `any(windows, target_os = "macos")`. ghost-core/ghost-cache/ghost-intent stay Windows-only. Linux stays excluded. Step C — `ghost doctor --mac`. Twelve capabilities driven against TextEdit, each timed and scored independently so one run says as much as possible about a machine we cannot log into. Emits JSON to stdout and to ~/.ghost/. Exit 0 only when every non-SKIP step passed; UNKNOWN counts as failure, because an unverified capability is what the command exists to eliminate. BackgroundDispatch is SKIP with a reason. capabilities_for(Platform::MacOS).functional remains false, and the report records that it was false while the evidence was gathered. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The first macOS compile of ghost-session and ghost-cli reported three dead-code errors and nothing else: off Windows, doctor.rs emits a single FAIL row, so its Win32 policy helpers and Status::Pass have no caller. Their tests are pure and worth running on both hosts, so the module allows dead code off Windows rather than losing the coverage. Session::new switches on `return` rather than trailing blocks: a cfg'd-out block still has to typecheck as a statement on the other host, and a statement block must evaluate to (). docs/mac-testing.md is written for the person who owns the Mac, not for a Ghost developer: two commands, what it will do to their machine, why macOS will ask for the permissions twice after a rebuild, and how to read the JSON. live_mac.rs is for a developer iterating on the backend on their own Mac, and is explicitly not the verification protocol — it needs #[ignore] plus GHOST_LIVE_MAC, and it puts the user's clipboard back. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three of ghost-session's integration tests name types that only exist on Windows (GhostSession, ghost_core::capture). They compiled everywhere until now only because the crate root carried #![cfg(windows)]; removing that gate to admit the macOS backend exposed them. `#[ignore]` does not help — an ignored test is still built. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t lines
ReflectionBuffer had an inherent `default()`, which clippy's
should_implement_trait flags. It went unnoticed because the module was only ever
compiled behind cfg(windows), where the Test job's clippy step already fails on
pre-existing ghost-core lints. Implementing Default is behaviour-preserving and
leaves every call site unchanged.
Two report strings in `doctor --mac`, which a Mac owner reads once and cannot
easily re-run:
- a PNG that fails to decode reported "PNG encode failed", describing the
opposite operation; CaptureUnusable renders it correctly.
- the window-enumeration row now says whether the app matched by title or by
owning pid. The neutral WindowRef carries the document title ("Untitled"),
so the pid arm is the one that normally fires, and a bare count could not
distinguish a working title match from a broken one.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two macOS-side lints in the new doctor + CLI code: - doctor_mac.rs:471 clippy::print_literal on the background-dispatch SKIP row; wrap in a scoped #[allow] with a comment (the aligned format is deliberate). - main.rs:661 clippy::cmp_owned on a PathBuf == PathBuf::from(...) comparison; switch to as_path() == Path::new(...) which is the idiomatic form. Five pre-existing Win32-side lints in ghost-core surfaced when the workspace clippy step ran on a newer clippy release. These predate this PR and are purely Windows-only code. Suppressed with an #![allow] block at ghost-core's crate root with a comment naming them as a follow-up cleanup pass, so this PR stays scoped to the macOS backend and does not silently rewrite Win32 logic. No behavior change; the goal is a green CI matrix so the mac backend can be handed to on-device testing.
Same follow-up-cleanup rationale as the previous ghost-core suppression.
…rs (windows-only)
Replaces the per-crate #![allow] blocks with a single [workspace.lints.clippy] table plus '[lints] workspace = true' opt-ins in every crate's Cargo.toml. Every allow is documented with the specific file/function it covers. All of them are pre-existing Windows-side lints surfaced by newer clippy releases on the windows-latest runner; none are caused by the macOS backend work. They are tracked as a follow-up cleanup pass; this PR stays scoped to the Mac backend. Motivation: chasing lints one commit at a time is not honest verification. The workspace table makes the set complete and reviewable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2 (
cross-platform-scaffold), so this diff currently includes thatbranch's commits. Base retargets to
maincleanly once #2 merges — CI onlytriggers on PRs against
main, which is why this is based there.Status right now
Build (macOS)job onmacos-latest— the authoritative proof gateRelease buildregression from Cross-platform scaffold: honest build gates + macOS/Linux plan #2 repaired (explicit-ppackage scope)crates/ghost-platform/src/macos/)SessionBackendextraction +Sessionenum inghost-sessionghost doctor --macdocs/mac-testing.mdHonesty
capabilities_for(Platform::MacOS).functionalstaysfalsefor the entirety ofthis PR and the #2 tripwire test is not weakened. Nothing is added to any
marketing surface. macOS is not "supported" until it has been run on a real Mac.