feat(macos): implement native screen capture and host input - #158
Draft
hyperb1iss wants to merge 143 commits into
Draft
feat(macos): implement native screen capture and host input#158hyperb1iss wants to merge 143 commits into
hyperb1iss wants to merge 143 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
hyperb1iss
force-pushed
the
nova/macos-capture-input
branch
from
August 13, 2026 15:16
b76fbd7 to
854f36f
Compare
Define the Sequoia-first architecture for ScreenCaptureKit, native host input, Metal publication, TCC ownership, and Tahoe capabilities. Carry Claude Opus review findings into explicit dependency, resource, permission, fidelity, packaging, and acceptance contracts. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com> Co-Authored-By: Claude Opus <noreply@anthropic.com>
ScreenCaptureKit lifecycle contracts rely on APIs introduced in macOS 15.2, so every build and distribution surface must reject older hosts. Force Cargo and Tauri to the same floor, teach Homebrew to distinguish 15.0 from 15.2, and gate the curl installer before network or launchd work. Co-Authored-By: Nova (OpenAI Codex GPT-5) <noreply@openai.com>
Intel Macs remain a first-class Sequoia target, but the release workflow only produced and consumed Apple Silicon tarballs. Add the Intel standalone lane, checksum and Homebrew plumbing, teach both installers the amd64 artifact name, and pin both native app lanes to the macOS 26 runner family. Homebrew now avoids respawning a clean ownership conflict exit. Co-Authored-By: Nova (OpenAI Codex GPT-5) <noreply@openai.com>
Run every Rust-changing pull request on native Apple Silicon and Intel Tahoe runners with the repository-pinned Xcode 26 SDK. Audit a finished Mach-O on both architectures and every macOS release payload so the 15.2 floor cannot drift before upload. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Give every app and standalone Mach-O a checked stable identifier and an explicit entitlement profile. The signing actor rejects ad-hoc identities, unknown code objects, signature drift, and unaccepted notarization before producing stapled app and DMG artifacts or standalone provenance. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Route native app and standalone macOS releases through the checked signing actor. Release jobs now require Developer ID credentials, notarize exact binary bits, publish provenance, and verify signatures, requirements, entitlements, staples, and accepted receipts before upload. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Teach shared configuration about ScreenCaptureKit's persistable source syntax and select the backend on macOS. Extend the canonical keyboard and media inventories with total Apple virtual-key mappings so future rows cannot silently omit a host backend. Pin the Objective-C framework bindings required by the native capture and input crates before those platform boundaries land. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Represent pointer scroll as signed Q16.16 values with independent line and pixel aggregates, lifecycle phases, and momentum phases. Route exact events through the canonical interaction batch while retaining a per-source fractional projector for the legacy vertical wheel signal. Expose exact scroll data to LightScript while preserving the existing wheel compatibility field. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Decode both Raw Input wheel axes into exact signed Q16.16 Line120 values. Core now publishes the canonical pointer-scroll event first and follows each integral vertical projection with one ordered legacy wheel shadow. Reset fractional projector state with device and session generations so gaps cannot leak stale motion into a replacement source. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Decode evdev vertical and horizontal wheel axes into exact signed Q16.16 Line120 events. Suppress low-resolution duplicates independently per axis when a device advertises matching high-resolution events. Publish integral vertical compatibility shadows and reset fractional state whenever the evdev source loses continuity. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Add exact two-axis scroll injection across the daemon and UI while retaining the legacy wheel wire shape for older clients. Core emits one ordered compatibility shadow only for integral vertical Line120 motion. Preserve DOM pixel precision, validate both Q16.16 axes with checked bounds, and reset fractional projection state whenever a browser source reconnects. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Carry exact scroll totals and ordered events through the production LightScript adapter. Replace optional mouse fields with a discriminated SDK union while retaining the deprecated wheel member for compatibility. Sanitize units, phases, deltas, and idle fallbacks so effects receive one stable shape inside and outside the daemon runtime. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
The Rust input contract already publishes integral 1/120-notch wheel units. Keep those values intact across the LightScript bridge instead of scaling state and events into fractional notches. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Define the plain Rust batch vocabulary, consent-specific event masks, media and pointer decoding, modifier flags, scroll phases, and validated virtual desktop geometry before introducing framework ownership. The contract compiles on every host. Core Graphics values stay out of hypercolor-core's portable folding layer. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Own separate listen-only keyboard and pointer taps on a dedicated Core Foundation run loop. Decode native keys, media controls, buttons, motion, scroll phases, and virtual desktop geometry into bounded Rust batches. Overflow and tap interruption publish ordered state barriers. Stop removes both tap sources before joining the worker and flushing the final barrier. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Connect the event-tap boundary to canonical held state, exact scroll, motion, lifecycle status, and generation fencing. A deterministic fixture covers partial permission, effective masks, epochs, and owner restarts. Preserve typed tap-disable reasons across the native boundary so stable failure codes distinguish timeout, user-input disable, and revocation. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Replace daemon startup with the native event-tap source while preserving keyboard and pointer consent independently. Map macOS permission failures through MCP health and cover both startup and diagnostic behavior. Delete the polling bridge, its tests, dependency graph, stale fixture labels, and lock inventory entry now that every supported host is event-driven. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Apple's SDK declares content and bounding rectangles in surface points, while dirty rectangles arrive in pixels. Preserve that boundary so capture validation never double-scales damage or applies content scale twice. Co-Authored-By: Nova (Codex GPT-5) <noreply@openai.com>
Model ScreenCaptureKit frames as validated plain Rust metadata backed by an opaque retained surface. Reject unknown formats, invalid plane arithmetic, malformed geometry, and incomplete color metadata before publication. Keep lifecycle-only statuses independent from image payloads and advance sequence numbers for complete frames only. Co-Authored-By: Nova (Codex GPT-5) <noreply@openai.com>
Screen capture must compile without Servo's heavyweight renderer stack. Make the existing context bridge opt-in and bind the core Servo feature to that exact edge so native capture can use IOSurface imports alone. Co-Authored-By: Nova (Codex GPT-5) <noreply@openai.com>
Validate ScreenCaptureKit samples into plain Rust frame metadata while retaining the originating CVPixelBuffer and its IOSurface lifetime. Keep the framework callback bounded with latest-value replacement and per-reason drop counters. Drive Apple's picker from the main thread and stage source changes in a candidate SCStream. A repick commits only after its first complete frame, so cancellation, malformed samples, and candidate failures preserve the active stream. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
Keep CVPixelBuffer locking, plane-address validation, and unlock symmetry inside the capture crate. Expose a BGRA row-copy operation that preserves source and destination padding without leaking Core Video types. Use cross-platform pixel fixtures so CPU publication shares the validated contract before signed hardware acceptance runs. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
Wrap picker ownership in dispatch2's MainThreadBound. The session handle can move with the core input source. Picker access and teardown still run on the process main thread. Pin the Send and Sync contract with a compile-time integration test. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
Retain the committed ScreenCaptureKit filter while zero demand stops active and candidate streams. Reactivation stages a fresh stream from that local filter without prompting or reopening Apple's picker. Promote a repicked filter only when its first complete frame activates. Candidate failure therefore preserves the last known-good selection. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
Decode sRGB, Rec.709, Rec.2020, and linear BGRA samples into linear light. Convert Display P3 and Rec.2020 primaries to sRGB, compress out-of-gamut results, and publish canonical RGBA8 bytes with preserved alpha. Reject PQ and HLG at the SDR seam. HDR cannot fall through as ordinary BGRA. Golden fixtures cover channel order, wide-gamut behavior, and rejection. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
Use a condition variable so conversion workers sleep until a frame arrives. Keep a bounded wait only for teardown responsiveness. Superseded frames continue to replace in constant space. Cover ready delivery and timeout behavior without timer polling. Co-Authored-By: Nova (GPT-5.6 Codex) <noreply@openai.com>
The native media worker only exists on Linux and Windows. Gate its private shutdown and failure-publication helpers so macOS lint does not compile dead branches that cannot execute there. Co-Authored-By: Nova (OpenAI Codex) <noreply@openai.com>
Bridge retained ScreenCaptureKit frames into core through an admitted BGRA8 conversion worker and a generation-fenced latest publication. Candidate workers start parked so failed thread creation preserves the live graph. Add deterministic fixture coverage for demand activation, publication, teardown, reconfiguration, and stale-generation retirement. Co-Authored-By: Nova (OpenAI Codex) <noreply@openai.com>
Separate privacy-bearing capture and input authority from loopback locality. Control credentials and trusted in-process transports can reach protected REST routes and WebSocket channels, while ordinary local lighting control keeps its credentialless compatibility. Reject sensitive subscriptions before demand registration and preserve the trusted authority through WebSocket command dispatch when API keys are off. Co-Authored-By: Nova (GPT-5 Codex) <noreply@openai.com>
Import signing passwords through a bounded Security.framework helper so certificate and keychain secrets never enter process arguments. Reject raw Apple ID password notarization and retain keychain-profile support locally. Keep Apple release credentials, signed artifacts, physical acceptance, and Homebrew promotion in the proprietary build system. Public CI now validates short-lived unsigned app fixtures and cannot select them for a release. Co-Authored-By: Nova (GPT-5.4) <noreply@openai.com>
Replace historical surface accounting with per-live admission tokens so IOSurface reuse and pinned generations retain exactly their native and heap allocations. Admit transient lease peaks before allocation and free index storage before releasing its charge. Preserve the GPU-only path and make additional live surfaces depend on real byte capacity instead of a fixed identity count. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Carry the complete owner topology, epoch, and process identity through handover. App sidecars stop only through their matching retained child with SIGTERM. Launchd and Homebrew stops stay bound to exact service identity under the owner publication lock. Fail closed when replay lacks stop authority, preserve version-one journals, and converge safely around newer same-topology publications. Reopen the app watchdog only after a terminal rollback proves the app sidecar is active. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Keep each capture admission owner alive beside its direct wgpu, Core Video, and native Metal wrapper. Reimports replace ownership under the cache lock, while explicit clearing releases only cache-held generations. Route retirement and Metal recovery clear every screen cache through one operation. Submitted GPU work keeps its own lease until completion, so cache turnover never shortens the GPU resource lifetime. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Publish lifecycle, health, freshness, pressure, and invalidation epoch from one locked observation. Terminal worker events clear every branch under one monotonic epoch. Recoverable failures retain last-good output. Fence prepared publications across invalidation and clear compositor output plus native GPU caches before a fresh publication can latch. Co-Authored-By: Nova (GPT-5) <noreply@openai.com>
Bind each daemon runtime to the canonical owner incarnation with a private per-user session artifact. Protected local control now requires the rotating session credential instead of treating loopback as identity. Publish only after the flock winner is established, expose only the public session ID, and clear the exact session after every normal exit.
Separate lifecycle control, latest complete frames, and recoverable diagnostics so frame pressure cannot erase terminal state. Bind async decode to a monotonic invalidation generation and serialize lifecycle transitions through the same generation authority. Co-Authored-By: Gibbs (Codex) <noreply@openai.com>
Keep the deprecated owner argument beside new environment metadata during the compatibility window, and require exact launcher corroboration before ownership. Move raw direct installs into a verified Rust transaction that stages immutable units before stopping a healthy daemon. Co-Authored-By: Huygens (Codex) <noreply@openai.com>
Serve the desktop shell from bundled Tauri assets and publish daemon routes only after health and ownership verification. Protected capture REST and WebSocket traffic receives a per-session control credential without granting remote documents application IPC. Bind the daemon's final API sockets before publishing the private session. A drop-ordered runtime authority keeps the exact listeners, owner guard, and attestation coherent across startup failures, shutdown, and unwinding. Screen pixels remain on the existing GPU transport. Tauri carries control metadata only. Co-Authored-By: Godel (GPT-5.6) <noreply@openai.com> Co-Authored-By: Nova (GPT-5.6) <noreply@openai.com>
The daemon now resolves its macOS owner from live process inspection (launchctl service pids, code-signed app-sidecar parent verification) instead of trusting the HYPERCOLOR_MACOS_OWNER environment claim or the --macos-owner flag; both remain claims that must corroborate the derived authority, and disagreement is fatal. Launchd and Homebrew service definitions gain the matching owner environment. Standalone is the user-directed residual category: no shell allowlist gates it, because a parent-binary name grants no authority (any launcher can interpose `sh -c`) and the allowlist broke cargo, sudo, and supervisor launches. Path comparison treats unresolvable paths as unattested rather than fatal, so bundle-shaped checks cannot abort startup on layouts (standalone, Homebrew, launchd) that have no app binary sibling, and the bundle-layout gate runs before the sibling probe it guards. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The stream transaction machinery moves out of native.rs into native/transactions.rs (claim-once cells, deadline scheduler, waiters) and native/lifecycle.rs, and the core capture control plus the TCC canary adopt the transaction API in place of raw channel receivers. Transaction cells now carry a mutable generation. When a source pick or interrupted-recovery restage adopts an in-flight request, the adopted cell is rekeyed to the new stage epoch; previously the cell stayed keyed to the superseded epoch, every generation-filtered arm, cancel, and claim missed it, the fresh candidate was insta-cancelled, and the core waiter either timed out spuriously on the old epoch's deadline or hung forever. Rekeying retires the old epoch's deadline in the same breath and bumps the deadline revision so in-flight timeout callbacks die on the revision check. Deadline arming validates the stage generation inside the cell's own critical section (arm_for_generation): an arm validated under the stream-state lock and preempted across an adoption rekey would otherwise re-install a deadline whose timeout hook targets the superseded stage and no-ops, wedging the adopted candidate. The render thread's capture-demand reconciler keeps retrying a failed demand application on every tick and paces only the warning: delaying the retry itself would hold capture inactive for the pacing interval after a transient failure, which the preview latency tests reject. Cancel hooks receive the generation the cell held at claim time instead of capturing a staging-time epoch that adoption makes stale, so a waiter cancel after adoption tears down the stage that actually owns the transaction. A completer drop guard cancel-claims the cell when the last completer clone drops, so no code path can strand a waiter; the guard deliberately skips the cancel hook, and claim retires hooks outside the cell lock so a hook that captured a completer cannot deadlock. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Tray quit runs app.exit(0), which terminates without unwinding, so the ManagedDaemon Drop kill never fired and the detached (process_group(0)) daemon survived as an orphan holding :9420 and the ownership guard. A fresh app launch then probed the stale daemon's health, failed owner verification five times, gave up, and rendered a black window. Three coupled fixes close every path: The app run loop handles RunEvent::Exit and reaps the managed daemon while the process is still alive: watchdog suppressed, graceful SIGTERM, a ten-second wait sized to the daemon's own shutdown budget (API drain, device blanking, persistence flush), hard kill as the fallback. Quit during the startup window, before a child is bound, falls back to a SIGTERM by pid. The daemon arms a parent-death watch when the supervisor hands it HYPERCOLOR_SUPERVISED_PARENT_PID: a change in getppid means the app died without reaping (crash, SIGKILL), and the daemon shuts itself down instead of orphaning. This is the unix mirror of the Windows job-object kill-on-close coupling. The watchdog routes all three "someone else holds our guard" signals into reclaim instead of the restart-budget crash loop: the terminal conflict exit (a same-owner contender loses guard arbitration within milliseconds), the owner-publication mismatch, and the session-verify failure. When the owner record names an app-sidecar pid this supervisor never spawned, the guard is actually contended, and the live process matches the recorded executable identity (canonicalized on both sides), the orphan is asked to terminate and the guard release is awaited before respawning. Reclaims charge the restart budget so a pathological reclaim loop still trips the circuit breaker. Spec 77 invariant 8 holds: the single-instance app is the owning topology reaping its own orphan. ManagedDaemon::Drop now kills unless the child has provably exited, so a try_wait error cannot leak a live process. The app-shell recovery predicate rearm and the webview hardening ride along: navigation in the credential-holding webview is restricted to the bundled tauri origin, and the private secure-input release selector warns once per process if WebKit ever renames it. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
…control POST /config/set could start screen capture, retarget the display, flip the microphone, or enable keyboard capture from any local unprivileged process with no credential, bypassing the gated picker that guards the identical capture.source mutation; POST /diagnose actuated a real screenshot-reference capture the same way; and the WebSocket interactive_preview_open verb raised real screen, audio, and interaction capture demand at the plain control tier, which unauthenticated loopback satisfies. All three now require the protected-control credential. Config writes gate the whole capture domain plus the enabling and device-retargeting leaves of audio and input (audio.enabled, audio.device, input.enabled, input.keyboard, input.mouse) and full resets, which rewrite them; DSP tuning and interaction routing policy stay credential-free so a keyless install keeps its sliders. Diagnose gates only requests that include the macos_screen_parity check, and the default check set stays open. The remaining preview verbs manage an already-authorized session and stay at the control tier. Route-level rejection tests cover the three endpoints, and the config integration tests ride the trusted in-process API like the other protected routes. Credential and API-key comparison switches to subtle's constant-time equality so match position cannot leak through timing; length still leaks, which is standard, since credential lengths are fixed and public. Audio device enumeration moves off the request thread. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Secure Keyboard Entry (a terminal holding the secure-input assertion) starves the event tap of keyboard events while pointer events keep flowing, so keys held at that moment stayed pressed forever and keyboard-reactive effects stayed lit. The 250ms health tick now polls IsSecureEventInputEnabled next to the existing TCC poll, emits an ordered SessionInterrupted gap on the rising edge (clearing held-key state), and publishes the observation into the session diagnostics so status snapshots read a cached value instead of calling Carbon from the render thread at frame rate. The flag surfaces on the platform status and the daemon status API, which also starts deriving audio availability from registered sources instead of probing host hardware; no pids or app names are logged. Native scroll deltas were 65536x too small: the reader used the integer accessor on the 16.16 fixed-point ScrollWheelEventFixedPtDeltaAxis fields, which rounds to whole notches and discards the raw bits the fold pipeline expects. It now reads the double accessor and scales back to Q16.16. Downstream units stay exactly as spec 76 defines them (mouse.wheel carries 1/120-notch units end to end); the keystrike effect, which still multiplied raw line120 by a per-notch factor, now converts to notches first. The event tap re-enables on every disable, including repeated ones: a disabled tap fires no callbacks, so the old give-up-after-two policy was permanent capture death with no recovery trigger. Retry cadence is bounded by the window server's own disable timeout, and repeats stay visible through diagnostics and the Degraded state. A dedicated run-loop stop source closes the shutdown race where CFRunLoopStop lands after the worker's stopping check but before CFRunLoopRun begins (a no-op that left stop() joining forever): the signaled source is serviced on loop entry and stops it from inside. The NSEvent media-key bridge runs inside an autorelease pool, and CapsLock's lock-state flag no longer counts its release re-report as an impossible edge. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
host_driven_animation now includes every HTML-sourced effect on macOS, including ones tagged webgl or canvas2d that previously kept their own requestAnimationFrame loops. With the native GPU interop path those effects render one frame per host tick like the rest of the catalog; other platforms keep the tag-based exclusion unchanged. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The branch's every_static_router_operation_is_cataloged test surfaced a pre-existing gap: the effect-preset routes (list and apply) have been in the router since before v0.3.2 but were never added to the hand-maintained catalog, keeping the daemon test lane red. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The macOS icon script becomes a one-line delegation to the canonical brand pipeline (assets/brand/build.py app-icon), which now emits the Pillow-written icns alongside the six Tauri icon assets and copies them into the app crate. The old qlmanage/sips/iconutil path was already dead: its source SVG no longer exists in the repo. The regenerated icon.png and icon.icns pick up the brand-master artwork with the ~5% safe-margin inset; the new icns drops the 1x 16pt and 32pt members (macOS downscales from the retained @2x set). The app bundle recipe gains an app-bundle-binaries stage so bundling builds the daemon and CLI sidecars (plus the SMBus broker and elevated helper on Windows) instead of staging whatever was left in target/release, and the packaging tests pin the launchd and Homebrew launcher definitions exactly, including the new owner environment. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
RELEASING.md now states what the tag lane actually does: no macOS artifacts and no Homebrew tap updates from public CI, because repository runners cannot perform Developer ID signing; signed macOS artifacts flow through the acceptance checkpoint and tap updates are manual until a signing-capable lane exists. HOMEBREW_TAP_TOKEN is marked unused. Spec 77 marks invariants 1 and 3 as target-state until H3.5 lands, naming the legacy CPU publication fallback as the one temporary mitigation so the GPU-only claim stops being false while the fallback is live. AGENTS.md documents the hypercolor-macos-owner crate in the project map and dependency graph. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The missing-device recovery test bounded background CoreAudio discovery at two seconds, which a fully loaded machine (the whole suite in parallel, or concurrent builds) starves reliably while solo runs finish in well under one. The bound only asserts that discovery terminates, so it now carries headroom instead of encoding a latency expectation the test never meant to make. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
hyperb1iss
force-pushed
the
nova/macos-capture-input
branch
from
August 15, 2026 23:39
854f36f to
c1d78b8
Compare
Unsigned local builds are ad-hoc signed, so their designated requirement is a bare cdhash with no identifier chain, and the signed parent/daemon requirement handshake can never succeed. The sidecar evidence then fell through to standalone, the supervisor's app-sidecar claim failed corroboration, and an unsigned bundle could not boot its daemon at all: on a host without a Developer ID identity there was no way to run the app. When the daemon's own requirement is ad-hoc shaped, sidecar evidence now rests on the structural chain alone: bundle layout, the exact adjacent app binary as parent, and the parent's live code identity executing that binary. A daemon whose requirement carries the identifier chain never takes this path, so signed builds keep the full handshake mandatory and fail closed. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The deep review that drove this branch's hardening pass lives with the PR it reviewed: verdict, both live symptoms, every finding with its receipt, the structural assessment of the ownership layer, and the addendum recording what the fix pass landed and which of the review's own claims the verification round corrected. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Sidecar evidence expected the launcher at Contents/MacOS/Hypercolor, but Tauri names the .app folder after the product while keeping the cargo binary name for the executable, so every real bundle ships hypercolor-app and the expectation was unsatisfiable, signed or not. The supervisor's app-sidecar claim then failed corroboration and the bundled app crash-looped to a blank window. The parent check now accepts the bundle's app binary names, with the canonical file name matched byte-for-byte: on the default case-insensitive APFS volume the product-cased candidate would otherwise resolve onto the lowercase CLI binary sitting in the same directory. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The CSP introduced with the trusted-UI session binding listed explicit schemes in connect-src but omitted 'self', and connect-src does not fall back to default-src. Inside the webview the SPA lives on tauri://localhost, so the wasm-bindgen loader's fetch of the WebAssembly module was refused by the policy, the UI never booted, and the window stayed black even with a fully healthy supervised daemon. Browsers hitting the daemon-served copy over http were unaffected, which is what made the webview look uniquely broken. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Tauri merges tauri.macos.conf.json over the base config, so the macOS webview enforces that file's loopback-scoped connect-src, not the base one; adding 'self' only to the base config changed nothing in the bundle. Both CSPs now allow same-origin connects, and the packaging tests pin the corrected directives. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The app-wide config was fetched exactly once at wasm init, with every non-auth error silently discarded. When the webview boots while the daemon is still binding, or outlives a daemon restart (the supervised sidecar makes that routine), that one fetch dies on a dead port and nothing ever retries: the Settings page shows its loading skeleton forever with no console output, while every hint-driven resource heals on the WebSocket reconnect. Observed live: a session whose boot burst fetched devices, scenes, and effects but never issued GET /api/v1/config, because the daemon respawned five minutes after the first instance and the webview predated it. The fetch now keys on the socket generation, so it refires on every WebSocket open, healing boot races and daemon restarts through the same reconnect that refreshes everything else. The swallowed error arm now logs a console warning so the next silent failure is visible. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The Input and Capture sections rendered the daemon's diagnostic snapshot verbatim: source identifiers (macos_host_input, cg_event_tap), state-machine vocabulary (configured / consented / demanded / freshness), capability owners, Tahoe HDR capability dumps, and requires_app_ui error codes. None of that means anything to a person configuring their lights, and an unsupported-on-macOS media source could push "run Hypercolor on Linux or Windows" into the input health banner as if it were actionable. Each section now shows at most one plain sentence: capturing, ready, or a warning with the daemon's remediation when there is one. The permission rows (Input Monitoring, Screen Recording) and the restart action remain the only calls to action, reworded in plain English. The per-source health cards, platform detail dumps, and their helpers are deleted outright; the daemon status API still carries the full diagnostics for anyone debugging. The input rollup now only listens to interaction sources. Media, network, and audio sources live in their own domains, so their issues can no longer degrade input health or leak remediations into the input section; a regression test pins the unsupported-media case. The macOS daemon-owner panel no longer renders an always-on card with the active owner, epoch, and a "local only" badge. It appears only when there is a real decision or event: two installs fighting over the lights, or recovery from an interrupted switch, both described in plain words. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Ad-hoc signatures carry a per-build cdhash designated requirement and a mangled per-build identifier, so macOS TCC treats every rebuild as a brand-new app: Screen Recording and Input Monitoring grants die on each just app-bundle while System Settings keeps showing the old build's toggle as enabled. Signing with a certificate gives the bundle a stable identity (identifier plus certificate leaf), so grants survive rebuilds. just app-bundle now resolves its signing identity through scripts/macos-dev-signing-identity.sh: an exported APPLE_SIGNING_IDENTITY wins, then a valid local "Hypercolor Dev" certificate, then explicit ad-hoc with a warning. The script prints the certificate hash rather than the name because a duplicate cert with the same label makes codesign reject the name as ambiguous. Non-macOS hosts resolve to ad-hoc and are unaffected. docs/development/DEV_SETUP.md documents the development environment for all three platforms: Linux system libraries and udev rules, the macOS signing certificate setup with the TCC rationale, and the Windows toolchain plus PawnIO hardware support. CONTRIBUTING.md links to it. Release signing and notarization are unchanged and stay with mac-installer. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Signing the dev bundle with a real certificate surfaced two failures an ad-hoc bundle never hits. Hardened-runtime library validation refused the Homebrew dylibs Servo links (freetype and friends carry a different team identity), killing the daemon at dyld load; the entitlements now include disable-library-validation, which release signing needs for the same reason. And Tauri signs the daemon sidecar with a filename-derived identifier, while the app-sidecar ownership handshake requires the designated requirement to open with the tech.hyperbliss.hypercolor.sidecar identifier and share its certificate tail with the app, so the daemon exited with an uncorroborated launcher claim. scripts/macos-dev-postsign.sh now re-signs the daemon with the sidecar identifier after the Tauri build and reseals the bundle, the same fix-up the release lane's manifest signing performs. Verified live: the certificate-signed bundle boots, the daemon claims app-sidecar ownership through the requirement handshake (not the ad-hoc structural fallback), and deep-strict verification passes. Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
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.
💜 What changed
This implements Spec 76 as Hypercolor's native macOS screen capture and host input architecture.
device_querypolling bridge with nativeCGEventTapkeyboard and pointer capture, independent consent, exact physical key identity, media keys, two-axis high-resolution scrolling, and event-driven publication.Architecture
The implementation is split across dedicated platform crates and existing canonical boundaries:
hypercolor-macos-inputowns native event-tap acquisition and decoding.hypercolor-macos-captureowns ScreenCaptureKit sessions and retained frame lifetimes.hypercolor-macos-gpu-interopowns IOSurface and Metal import plus native reduction.hypercolor-macos-ownerowns durable process topology arbitration and handover state.hypercolor-coreowns canonical input folding, demand, publication, processing, and status.hypercolor-daemonowns renderer integration, protected local actions, diagnostics, telemetry, and API publication.Candidate capture streams, request changes, source selections, and worker replacements commit only after validated first delivery. Old generations stay retained through GPU completion, while stale callbacks and terminal errors are fenced by exact lifecycle identities.
🔮 Deep review and hardening pass
A full multi-lens review of this branch (docs/design/71) drove a hardening pass that is now part of this PR:
Receipts on this head: macos-capture 129 passed, launcher authority 10 passed, daemon openapi 4 passed, SDK 183 passed with typecheck and Biome clean, and workspace clippy clean at deny-warnings. One caveat: the render-thread screen-preview test family is timing-sensitive under intra-binary parallelism on high-core machines (46/46 pass single-threaded, and main flakes there too); it is tracked separately and CI's runners pass it.
Unsigned local bundles boot: when the daemon's own signature is ad-hoc, sidecar evidence rests on the structural chain (bundle layout, exact adjacent app binary, live parent code identity) instead of the identifier handshake that only signed builds can satisfy; signed builds keep the full handshake mandatory.
Deferred and tracked as follow-ups rather than fixed here: the consent model for capture-reactive effect applies, MCP tool authentication (both pre-existing surfaces), and the platform-layer restructuring, which now has its own abstraction-level review and spec in
docs/design/72-cross-platform-boundary-review.mdon main and is deliberately not part of this PR.Verification
The final reversible implementation received an independent requirement-by-requirement PASS against Spec 76. No remaining source, deterministic-test, CI, packaging, generated-client, or documentation gap was found.
Focused receipts on the final branch include:
The active Apple M3 Pro also completed the required 100-iteration Metal 4 qualification. CPU, wgpu Metal, and direct Metal 4 output matched exactly. Metal 4 GPU reduction p95 was 204,500 ns versus 202,625 ns for wgpu, so the typed production decision is
rejectbecause it does not clear the required 10 percent improvement.📦 Release lane reality
Public CI ships no macOS artifacts and does not update the Homebrew tap: repository runners cannot perform Developer ID signing, so macOS binaries flow through the signed acceptance checkpoint below and tap updates stay manual until a signing-capable lane exists. The AUR lane is unchanged, and RELEASING.md documents the current shape.
Physical acceptance boundary
This remains a draft until the signed physical acceptance bundle exists. Fixtures and builds do not substitute for the release gate.
Still required before a release can claim Spec 76 acceptance:
The current development host has Xcode 26.6 and an Apple M3 Pro, but no Developer ID identity or notarization credentials. Opening this PR gives the hosted Intel lane the first available architecture-specific compile and fixture pass without weakening the physical release gate.