Skip to content

macOS: ShadowCode runs as a native Mac app - #7

Merged
ShadowfetchStudio merged 18 commits into
mainfrom
feat/macos-phase1
Oct 17, 2026
Merged

ShadowfetchStudio merged 18 commits into
mainfrom
feat/macos-phase1

Conversation

@ShadowfetchStudio

@ShadowfetchStudio ShadowfetchStudio commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Makes ShadowCode build, install and run as a native macOS app. Tested on an Intel iMac (macOS 15, Xcode 26.3): scripts/build-macos-app.sh produces ShadowCode.app and ShadowCode_<version>_<arch>.dmg (ad-hoc signed, macOS 12+); the app opens from Finder, doctor passes, and the local engine runs llama.cpp on Metal.

What changed for macOS

  • Builds and tests: the workspace compiles and is clippy-clean on macOS; the engine's unit and integration tests pass there. .github/workflows/macos.yml runs clippy and the unit tests on macos-15.
  • Paths: ~/Library/Application Support/ShadowCode (cache in ~/Library/Caches); sessions and worktrees record real paths (/var is /private/var).
  • PATH from Finder: the login shell's PATH is adopted once at startup.
  • Shell sandbox: Seatbelt profile mirroring bubblewrap (hidden home and other volumes, empty HOME with read-only toolchains, writes only in the project and scratch, network off/on/allow-list proxy).
  • Keychain for API keys (security-framework).
  • Processes: descendants via proc_listchildpids; helpers die with ShadowCode through a kqueue watcher (Linux keeps PR_SET_PDEATHSIG).
  • Terminals, dev-server preview, worktree cleanup, SQLite inspection, installed memory ported off /proc.
  • Local models: scripts/build-llama.cpp.sh builds a Metal runtime with precompiled shaders; bundled in Contents/Resources/llama.cpp.
  • Linux behaviour is unchanged; tests that read /proc now use portable helpers.

Distribution (GitHub only)

  • .github/workflows/release-macos.yml: on a published release, builds the .dmg on macos-15 (Apple silicon) and macos-15-intel, smoke-tests it and attaches both plus ShadowCode_<version>_macos.sha256. On PRs touching macOS packaging it only builds.
  • scripts/install-macos.sh: curl … | bash installer that checks the checksum and installs to /Applications; curl downloads are not quarantined, so the un-notarized app opens without a Gatekeeper block. Browser downloads need Privacy & Security › Open Anyway once (README).
  • The Linux publisher ignores those macOS files on a retry of an already published release; the updater shows the installer command on Macs.

Not in this PR

  • Publisher signatures for the macOS images (needs macOS targets in release/trust/policy); they are checked by SHA-256 over HTTPS.

See docs/MACOS_PORT_ESTIMATE.md › Progress for details.

🤖 Generated with Claude Code

- Voice capture: cpal is a macOS dependency too (CoreAudio).
- Control socket probe: macOS has no SOCK_NONBLOCK/SOCK_CLOEXEC; set both
  with fcntl(2) before the descriptor is used.
- Lints that only fire on macOS: a test-only worktree helper is gated like
  its Linux-only caller, a test's struct update is allowed (its other fields
  exist on Linux only), and the Wayland window variant is allowed unused off
  Linux (macOS sizes like X11).
- CI: a macOS job builds the interface and runs clippy for the whole
  workspace with warnings as errors.
- docs/MACOS_PORT_ESTIMATE.md records phase 1 and the first phase 2
  findings (604 of 638 engine unit tests pass on macOS).

Verified on macOS 15.8.1 (iMac19,2, Xcode 26.3): cargo clippy --workspace
--all-targets --locked -- -D warnings is clean. Linux: fmt, clippy and the
affected modules' tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a28e3eff-3874-4597-9e6d-9ea2aba7fa7f
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ShadowfetchStudio and others added 15 commits October 16, 2026 13:40
Workspace file and folder opens pass O_NOFOLLOW to cap-std. On Linux cap-std
opens with openat2 and that is enough; on macOS it resolves the path itself
and followed a symlink at the last component before the flag reached the
final openat, so mutation_leaf_opens_refuse_symlinks_and_nonregular_files
failed there. Every such open now also sets cap-std's own
FollowSymlinks::No (cap-fs-ext 3.4.6, the same release as cap-std).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workspace root is canonical, so an absolute path naming it through a
symlink was refused as outside the workspace. On macOS that is every
temporary folder (/var is /private/var) and any project reached through a
symlinked folder; 14 engine tests failed there for this reason. When the
plain prefix check fails, the deepest existing ancestor is canonicalized and
the check runs again; a path that really resolves outside, by .. or by a
symlink inside the workspace, stays refused. Tested on both systems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixed

- Absolute paths naming the workspace through a symlinked folder stay
  inside (on macOS /var is /private/var, as is every temporary folder).
- Remote access refuses a not-yet-existing folder under a symlinked
  profile path too (it resolved only existing paths; both systems).
- Backups locate their open folder with fcntl(F_GETPATH) on macOS, where
  there is no /proc/self/fd, so a symlink swapped in at the folder's name is
  never written through (secrets included).
- Rules exported to a vendor CLI are recognised as linked by the links' real
  location, not their spelling.
- Test fixtures name their temporary folders canonically, as product paths
  are; the import-label test no longer requires the redacted address.
- docs/MACOS_PORT_ESTIMATE.md: phase 2 progress and what remains.

macOS 15.8.1 (iMac19,2): engine unit tests 640 passed, 0 failed; clippy
-D warnings clean. Linux: 664 passed; fmt and clippy clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the temporary and data folders are reached through /var, a link
to /private/var. Sessions are matched against Workspace::open, and
managed checkouts are checked against their canonical path, so both now
store the real path. The macOS integration failures drop from 118 to 43.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- SQLite inspection: macOS has no /proc/self/fd. Names under a reserved
  prefix stand for the held directory, and SQLite's own open/stat calls
  for them become openat/fstatat on that directory (never following a
  final link); every other name passes through unchanged.
- Terminals: openpty on macOS, and session members found through
  proc_listallpids and getsid, so hang-up reaches job-control groups.
- Local models: installed memory from hw.memsize (was 0 GB on macOS).
- Rulebook: a profile item's id stays relative when discovery names it by
  its real path.
- Tests: process liveness through one helper (proc_pidinfo on macOS)
  instead of /proc, which passed vacuously there; real paths where the
  service reports them; no GNU-only touch -d or setsid(1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- worktrees::cleanup (keeps a checkout deleted outside ShadowCode
  recoverable) now runs on macOS: the held directory is named with
  F_GETPATH and the exclusive rename is renameatx_np(RENAME_EXCL). The
  looser non-Linux fallback is removed.
- Tests: forged worktree records use the real checkouts path; two
  paused-clock stream tests are Linux-only (the paused clock races real
  loopback I/O on macOS; the timing logic is the same everywhere).
- docs/MACOS_PORT_ESTIMATE.md: integration status and how to run it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Settings and history live under ~/Library/Application Support/ShadowCode
  (cache under ~/Library/Caches) on macOS; an XDG variable set on purpose
  still wins.
- Opened from Finder or the Dock, the app inherits launchd's minimal PATH.
  Without a terminal it now asks the login shell for its PATH once at
  startup (5 s limit, common install folders otherwise), so Homebrew Git,
  Node and the vendor CLIs are found.
- Stopping a command or vendor agent signals its descendants on macOS
  too (proc_listchildpids), not only its process group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent's command tool used bubblewrap or Landlock, so on macOS it ran
commands unsandboxed. It now runs them under sandbox-exec with a profile
that mirrors the bubblewrap one:

- Reading only in system and developer folders, the prefixes of the
  programs on PATH outside the home folder (Homebrew), the project, the
  toolchain folders and the command's scratch. The rest of the home
  folder, other users and other volumes are hidden; even stat is refused
  except on the folders leading to an allowed one.
- HOME is an empty folder in the scratch linking to the read-only
  toolchains; TMPDIR is the scratch. Credential folders are never linked
  and stay unreadable inside the project too.
- Writes only in the project and the scratch (and xcrun's cache files).
- Network off allows no IP networking, not even localhost; on allows it;
  the allow-list allows only the proxy's loopback port.
- DEVELOPER_DIR is set so Apple's git/clang shims don't run xcodebuild
  (3.6 s and warnings per call inside the sandbox; 0.08 s with it).
- Signals only within the sandbox; system services only for user
  lookups, logging, file events and (with network) DNS and certificates.

'Require sandbox' and the allow-list refuse commands when sandbox-exec
cannot run. Settings, status and doctor describe the macOS sandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyring was the freedesktop Secret Service over D-Bus only. On macOS
the same "Move to keyring" now keeps a key in the login Keychain as a
generic password (service ShadowCode, account <profile>/<name>) through
security-framework (MIT OR Apache-2.0, macOS only). Settings › Accounts
names the Keychain there. A live round trip against the real Keychain is
an ignored test (it needs a logged-in desktop session; SSH sessions are
refused by macOS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview found the project's servers through /proc, so it was left out
of macOS builds. On macOS it lists this user's listening TCP sockets with
the system's lsof and reads each process's working folder and executable
through libproc; the proxy and the rest are unchanged. Platform-neutral
servers/own_ports_now/listening_now pick the source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview integration test, the vendor login supervision tests and two
vendor unit tests were Linux-only because they read /proc to see whether
a process is alive or reaped. They use kill(pid, 0) or, where process
identity matters, proc_pidinfo (start time, zombie state) and
proc_pidpath on macOS, and pass there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Model servers, language servers, embeddings, vendor agents and login
  helpers used Linux's PR_SET_PDEATHSIG so a crash never leaves them
  running. macOS has no equivalent: the child forks a watcher (no
  inherited descriptors) that waits on kqueue and kills the child if
  ShadowCode exits first. One helper, process::die_with_parent, now
  serves every call site, with a test that SIGKILLs a stand-in parent.
- scripts/build-llama.cpp.sh builds a Metal runtime on macOS (embedded
  shaders, the same loadable CPU variants, @loader_path), works with
  macOS's bash 3.2, and installs to ~/Library/Application Support/
  ShadowCode/llama.cpp, where the app now looks; the bundled runtime is
  Contents/Resources/llama.cpp.
- tauri.macos.conf.json: app and dmg bundles with the runtime, ad-hoc
  signed, macOS 12 or later. Info.plist explains microphone use.
- Finder's -psn_ argument is ignored instead of failing the parse.
- The silent-download test is Linux-only like the stream tests: its
  paused clock races real loopback I/O on macOS (CI failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The macOS runtime compiles its Metal shaders at build time
  (default.metallib next to llama-server). Compiling them at run time
  took 55 s on first use and hung outside a desktop session (over SSH or
  as a background service); --list-devices now answers in 0.2 s either
  way. Only the libraries llama-server and llama-cli use are shipped.
- llama.cpp names Metal devices MTL0; the backend is reported as metal
  and Settings › Local models says Metal.
- scripts/build-macos-app.sh builds the runtime when needed and then
  ShadowCode.app and the .dmg with it in Contents/Resources/llama.cpp.
  The runtime mapping lives in packaging/macos/bundle-runtime.conf.json,
  used only there: tauri-build refuses a missing resource path, which
  broke the macOS CI build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local engine's hardware line said "via MTL" for llama.cpp's Metal
device ids. docs/MACOS_PORT_ESTIMATE.md records what now works on macOS
(the app bundle, sandbox, Keychain, processes, preview, Metal runtime)
and what remains (Developer ID signing and notarization, release
automation and updates, Apple silicon testing, a real model run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShadowfetchStudio ShadowfetchStudio changed the title macOS port: phase 1 (compiles and passes clippy on macOS) and phase 2 groundwork macOS: ShadowCode runs as a native Mac app Oct 17, 2026
ShadowfetchStudio and others added 2 commits October 16, 2026 21:58
ShadowCode for macOS is distributed on GitHub only, not notarized:

- release-macos.yml builds ShadowCode.app/.dmg on Apple silicon
  (macos-15) and Intel (macos-15-intel) runners when a release is
  published, smoke-tests the app (version, doctor's sandbox check, the
  bundled Metal runtime, hdiutil verify) and attaches both images with
  ShadowCode_<version>_macos.sha256, never replacing an existing file.
  Pull requests that touch macOS packaging only build.
- scripts/install-macos.sh picks the Mac's image (Rosetta-aware), refuses
  it unless the release checksum matches, installs into /Applications
  (~/Applications when not writable) and links ~/.local/bin/shadowcode.
  curl downloads are not quarantined, so Gatekeeper does not block it.
- The Linux publisher, on a retry of an already published release, leaves
  those macOS files alone (a draft still refuses them).
- The updater knows the macOS app and shows the installer command.
- README: macOS install, the browser-download "Open Anyway" step, and
  where data lives on a Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Redaction's high-entropy rule masked whole absolute paths such as
  /Volumes/NVME1TB/shadowcode-macos/task-demo (long, mixed case, digits
  and '/', like base64), so a command's folder showed as "[redacted
  secret]" on its approval card; on macOS that is most project paths. A
  path under a well-known root (/Users, /Volumes, /private, /home, ...,
  also after file://) is now judged one segment at a time: a secret that
  is itself a segment is still hidden, and base64 that only contains '/'
  is judged whole as before.
- Whether a local model fits was judged from installed memory only. The
  engine now also reads memory free right now (MemAvailable on Linux; free,
  inactive, speculative and purgeable pages on macOS), and a model that
  runs in RAM, or in Apple silicon's shared memory, says "Only N GB of
  memory is free right now; close other apps first or it will run very
  slowly" when it would not fit. Seen on an 8 GB iMac whose iOS Simulator
  held most of its memory: Granite 3B ran at 6 s per token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShadowfetchStudio
ShadowfetchStudio marked this pull request as ready for review October 17, 2026 16:03
@ShadowfetchStudio
ShadowfetchStudio merged commit 0c86633 into main Oct 17, 2026
13 checks passed
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