Skip to content

release: v0.1.0 daemon hardening, wake recovery, and source/Homebrew packaging - #16

Merged
mitchelljphayes merged 3 commits into
mainfrom
fix-wake-remap-reapply
Sep 5, 2026
Merged

mitchelljphayes merged 3 commits into
mainfrom
fix-wake-remap-reapply

Conversation

@mitchelljphayes

Copy link
Copy Markdown
Owner

Summary

Lands the v0.1.0 candidate for source distribution and the Homebrew
source-build Formula. The previous commit be8c333 (daemon hardening,
installers, Raycast ext, CI) is already on main; this PR layers the
wake-from-sleep recovery, source/Homebrew packaging, and Raycast
identity/dep updates on top.

What this PR adds

  • Wake-from-sleep recovery (src/wake.rs, src/main.rs,
    src/macos_ffi.rs, src/hidutil.rs): IOKit IORegisterForSystemPower
    watcher with a ~2 s debounced one-shot timer (CFRunLoopTimer) that
    re-applies Switcheroo's owned modifier remaps through the existing
    idempotent hidutil path. All live hidutil transactions are serialized
    through HIDUTIL_TRANSACTION_LOCK; the panic hook uses try_lock
    and falls back to the durable state file when the lock is held or
    poisoned. extern "C" trampolines catch panics. PowerWatcher
    cleanup follows Apple QA1340's deterministic Drop order.

  • Source + Homebrew packaging (scripts/package.sh,
    scripts/version-sync.sh, packaging/homebrew/,
    rust-toolchain.toml, bundle/AppIcon-1024.png, install.sh):
    deterministic source/binary archive builder with isolated
    CARGO_TARGET_DIR, fresh-private mktemp output, hermetic
    release-mode env -i build with private HOME/CARGO_HOME and
    --frozen, source/binary byte-identity reproducibility check,
    deterministic manifest bound to the validated commit SHA. Homebrew
    Formula template builds from source via cargo install *std_cargo_args; render_formula.sh builds the production
    Formula from strict-semver version + 64-hex SHA-256 (URL
    constructed internally) with a rehearsal-mode safe-copy path and
    DO NOT PUBLISH marker. rust-toolchain.toml pins rustc 1.95.0.

  • CI hardening (.github/workflows/ci.yml,
    .github/workflows/release.yml): macos-14-pinned runners,
    full-SHA-pinned actions, persist-credentials: false,
    --locked clippy/test/build-all-targets/build-all-features gates,
    SHA-pinned cargo-deny advisories check, shellcheck + npm audit +
    raycast lint:ci (eslint + prettier). release.yml is a
    read-only tagged-release artifact pipeline (contents: read only)
    gated on dispatch-from-main, validated semver tag with main-ancestor
    commit, version surfaces verified at the validated SHA, and
    scripts/package.sh release mode invoked with the validated commit
    SHA. private-validation-artifacts upload only — no GitHub
    Release is created
    .

  • Raycast extension (raycast-extension/): lint:ci script
    added (eslint + prettier), service.ts / service-pure.mjs /
    service.test.mjs split for testable pure helpers with full Homebrew
    identity validation, exact executable matching against the two
    official opt_prefix paths, pre-kickstart re-verification, and 22
    service unit tests.

  • Documentation (README.md, CHANGELOG.md, RELEASING.md):
    Homebrew source-build is now Option 0 (recommended public path);
    binary archive Option A demoted with explicit nonpublic-distribution
    warnings. CHANGELOG.md has the 0.1.0 entry (Added / Security /
    Known Limitations). RELEASING.md is the maintainer release runbook
    (version bump, gates, Homebrew rehearsal, GitHub Actions release
    workflow, publication allowlist, later authorized actions, known
    gaps).

Test gates (carried forward, focused re-verification 2026-09-06)

Gate Result
cargo fmt --check ✅
cargo clippy --locked --all-targets --all-features -- -D warnings ✅ 0 warn / 0 err
cargo test --locked ✅ 126 / 126
cargo build --locked --all-targets --all-features ✅
shellcheck (lib, test_lib, package, version-sync, install, install-binary, uninstall, render_formula) ✅ 0
scripts/test_lib.sh ✅ 24 / 24
scripts/version-sync.sh ✅ 4 / 0
actionlint ✅ 0
zizmor . ✅ 0 (11 suppressions)
package.sh dry-run + full rehearsal + --verify-reproducibility byte-identity ✅
Raycast eslint + prettier + tsc --noEmit + npm run build + node --test ✅ 22 / 22
Raycast npm audit ✅ 0 vuln
Workflow SHA pins (all uses:) ✅ 34 pinned
Workflow permissions (contents: read only) ✅

Distribution policy (explicit, per RELEASING.md)

  • No public binary distribution. Prebuilt archives are produced by
    CI for validation only — they are unnotarized, ad-hoc-signed
    (self-consistency only, NOT authenticity), and must NOT be
    attached to a public GitHub Release
    . Co-hosted checksums provide
    integrity only, not authenticity. The sole public install path is
    the Homebrew source-build Formula
    .
  • No Raycast Store publication in this pass — the Raycast
    extension package.json prepublishOnly deliberately fails; no
    publish script exists.
  • Actual v0.1.0 tag and Formula checksum are NOT created in this
    PR.
    They require the authorized post-merge step (compute the
    SHA-256 of the public tag archive bytes, render
    packaging/homebrew/switcheroo.rb via
    packaging/homebrew/render_formula.sh, create the
    mitchelljphayes/homebrew-switcheroo tap, push
    Formula/switcheroo.rb).

Manual verification notes

  • The wake-recovery path is user-reported to work (manual
    verification on the maintainer's machine after sleep/wake cycles),
    but the current daemon install/version was not independently
    verified
    end-to-end by this PR's author on a fresh account. The
    IOKit lifecycle follows Apple QA1340 and the unit tests cover the
    policy/transaction/rollback edges, but a real-world
    install → sleep → wake → shutdown cycle on a clean account is
    recommended before tagging.
  • The Raycast Store publication / ray lint author check is external
    and not in scope; the repo-controlled quality gates
    (lint:ci = eslint + prettier, tsc, build,
    node --test, audit) all pass on the committed code.

Out of scope / follow-ups

  • RELEASE_SETUP.md prerequisites (release environment, env-scoped
    sentinel secret, branch protection, immutable v* tag ruleset,
    Actions SHA-pin policy, credential incident resolution) are external
    governance actions — not code changes — and remain out of scope for
    this PR.
  • Notarized binary publication / signed manifest + attestation
    deferred (prebuilt binary remains CI-only).
  • Keyboard hot-plug (IOServiceAddMatchingNotification) is
    deferred; the ReapplyPolicy / reapply_remappings seam is
    reusable for a future DeviceTrigger.

Reviews / security

  • Session code review (review.md): PASS for v0.1.0 source
    release + Homebrew source formula. Zero code-level release blockers.
  • Session security audit (security-review.md): no Critical / High /
    Medium code-level blockers for commit, push, PR. Prior High cleanup
    hazard verified fixed. All public-distribution surfaces are
    fail-closed.

Files

34 files changed, 4836 insertions(+), 298 deletions(-) in the new
commit 25ad011 on top of be8c333. .githooks/pre-commit,
.githooks/post-checkout, and .githooks/pre-commit-user (GitButler
state) are intentionally NOT included in this PR.

- Daemon (src/): hardened lifecycle — home dir resolution, safer
  hidutil mapping ownership, atomic apply/rollback, panic-safe
  cleanup, tighter error handling across config/engine/event_tap,
  and removed unsafe surface from macos_ffi.
- Installers (install.sh, uninstall.sh, install-binary.sh,
  scripts/lib.sh, scripts/test_lib.sh): fail-closed plist identity
  checks, race-protected rollback, helper-based path/sed/launchctl
  utilities, and shell-level test coverage for the install flow.
- Raycast extension (raycast-extension/): dependency updates and
  service/client compatibility adjustments for the hardened daemon.
- CI (.github/workflows/release.yml): read-only tagged-release
  artifact pipeline (no contents: write).
- Docs (README.md, RELEASE_SETUP.md, bundle/Info.plist): updated for
  the new bundle id and release flow.
Adds the v0.1.0 release candidates for source distribution and Homebrew
source-build formula. Layered changes are committed together to preserve
the cross-cutting README/CHANGELOG/RELEASING updates.

Wake recovery (src/):
- src/wake.rs: ReapplyPolicy (pure debounce seam), reapply_remappings,
  publish_applied_mappings, and install_power_watcher (production glue
  on the main CFRunLoop). Serializes live hidutil transactions through
  HIDUTIL_TRANSACTION_LOCK so wake reapplies cannot interleave with
  panic/signal cleanup; the panic hook uses try_lock and falls back to
  durable state-file reconciliation when the lock is held or poisoned.
- src/main.rs: SHUTTING_DOWN flag, transaction lock, panic-cleanup
  helper extracted for testing, and install_power_watcher wired before
  event_tap::run.
- src/macos_ffi.rs: PowerWatcher wraps the IOKit system-power lifecycle
  per Apple QA1340 with deterministic Drop ordering; CFRunLoopTimer
  drives the one-shot debounce. Both extern "C" trampolines catch
  panics so panic-across-FFI cannot abort the process.
- src/hidutil.rs: with_isolated_state_test pub(crate) seam for wake.rs
  tests; AppliedMappings derives PartialEq/Eq for assertion clarity.
- src/{config,engine}.rs: minor wiring updates.

Source/Homebrew packaging (scripts/, packaging/, rust-toolchain.toml,
bundle/AppIcon-1024.png, install.sh, .gitignore):
- scripts/package.sh: deterministic source + binary archive builder
  with isolated CARGO_TARGET_DIR, fresh-private mktemp output under
  validated owned parent, absolute-path shell utilities, source/binary
  archive byte-identity reproducibility check via dedicated run-2
  parent, release-mode hermetic env -i build with private
  HOME/CARGO_HOME and --frozen, deterministic manifest bound to the
  validated commit SHA, and an explicit REHEARSAL vs RELEASE marker.
- scripts/version-sync.sh: validates Cargo.toml/Info.plist/CHANGELOG/
  binary/tag version surfaces against the canonical Cargo version.
- packaging/homebrew/: Formula template (switcheroo.rb.tpl) with
  source-build cargo install, switcheroo.rb service do block, sample
  config under etc/, ad-hoc signing, and Accessibility caveats;
  render_formula.sh builds the production Formula from strict-semver
  version + 64-hex SHA-256 (URL constructed internally) with a
  rehearsal-mode safe-copy path and explicit DO NOT PUBLISH marker.
- rust-toolchain.toml: pins rustc 1.95.0 for reproducible builds; CI
  and local release builds resolve to this toolchain.
- bundle/AppIcon-1024.png: 1024x1024 PNG master used by both the
  packaging path and (via sips/iconutil) the install.sh fallback.
- install.sh: --locked build, fail-closed binary-version stamping
  of Info.plist from built binary --version (strict-semver verified
  against cargo metadata, mismatch aborts).
- .gitignore: dist/, dist-parent/, rendered Homebrew formula.

Documentation (README.md, CHANGELOG.md, RELEASING.md):
- README.md: Homebrew source-build is now Option 0 (recommended public
  path); binary archive Option A demoted with explicit nonpublic-
  distribution warnings (ad-hoc, unnotarized, integrity-only checksums).
- CHANGELOG.md: 0.1.0 entry with Added, Security, Known Limitations.
- RELEASING.md: maintainer release runbook (version bump, gates,
  Homebrew rehearsal, GitHub Actions release workflow, publication
  allowlist, later authorized actions, known gaps).

CI hardening (.github/workflows/ci.yml, .github/workflows/release.yml):
- ci.yml: macos-14 pinned, full-SHA-pinned actions, persist-credentials
  false, --locked clippy/test/build-all-targets/build-all-features
  gates, SHA-pinned cargo-deny advisories check, shellcheck + npm
  install audit + raycast lint:ci gate (eslint + prettier).
- release.yml: read-only tagged-release artifact pipeline (contents:
  read only), dispatch-from-main gate, validated semver tag with
  main-ancestor commit, version surfaces verified at the validated
  SHA, scripts/package.sh release mode invoked with the validated
  commit SHA, private-validation-artifacts upload only (no Release).

Raycast extension (raycast-extension/):
- lint:ci script added (eslint + prettier), explicit lint workflow,
  service.ts/service-pure.mjs/service.test.mjs split for testable
  pure helpers with full Homebrew identity validation, exact
  executable matching against the two official opt prefixes,
  pre-kickstart re-verification, and 22 service unit tests.
- README, eslint.config.js, refreshed command icon, dependency
  updates and client/service compatibility for the hardened daemon.

Test gates (carried forward per session):
- cargo fmt --check, clippy --locked --all-targets --all-features,
  test --locked (126/126), build (all-targets + all-features).
- shellcheck (lib, test_lib, package, version-sync, install,
  install-binary, uninstall, render_formula) clean.
- scripts/test_lib.sh 24/24, scripts/version-sync.sh clean.
- actionlint and zizmor (11 suppressions) clean.
- package.sh dry-run + full rehearsal + --verify-reproducibility
  byte-identity.
- Raycast eslint/prettier/tsc/build/--test 22/22 + npm audit 0.

Public distribution is source-only: prebuilt binary archives remain
unnotarized/ad-hoc-signed CI validation artifacts (never attached to
a public GitHub Release); the only public install path is the
Homebrew source-build Formula (post-merge: compute the real SHA-256
of the v0.1.0 tag archive and render the production Formula).
Actual v0.1.0 tag and Formula checksum are NOT created in this
commit — they require the authorized merge-to-main step.
The pinned SHA of `dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772`
(`v1`) declares `toolchain` as `required: true` and does NOT auto-detect
from `rust-toolchain.toml`. The previous commit added `rust-toolchain.toml`
to the repo but did not pass an explicit `toolchain:` input, so all Rust
gates (Format / Clippy / Test / Build / Package dry-run) failed in
`fix-wake-remap-reapply` CI with the action's parse step:

    'toolchain' is a required input

Adding `toolchain: 1.95.0` (matching the pinned `rust-toolchain.toml`)
to every `dtolnay/rust-toolchain` invocation in `ci.yml` (6 sites)
and `release.yml` (4 sites) restores the gates. The value matches the
pinned rustc; either source (`rust-toolchain.toml` or `toolchain:`
input) overrides the other, and the explicit input is the safer of the
two because it survives a future removal of the file.

Verified locally with actionlint (0) and zizmor (0, 11 suppressed).
@mitchelljphayes
mitchelljphayes merged commit 09181df into main Sep 5, 2026
11 checks passed
@mitchelljphayes
mitchelljphayes deleted the fix-wake-remap-reapply branch September 5, 2026 22:16
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