mac signing (debug builds)

scripts/package-mac-app.sh builds a staged app, calls scripts/codesign-mac-app.sh, and verifies the signed runtime before replacing dist/Apus.app. macOS ties TCC permissions to the bundle ID and code signature. Keep both stable across rebuilds, and keep the app at a fixed path. macOS then keeps its TCC grants (notifications, accessibility, screen recording, mic, speech).

The private Bun runtime can load native addons from npm plugins installed at runtime, whose binaries are not signed by the app's Team ID. Disabling library validation on this executable matches the official Node and Bun macOS binaries and the external Gateway's previous Node runtime. The runtime already executes arbitrary plugin JavaScript and spawns processes, so library validation on it is not a meaningful security boundary. Bundled natives remain Team-signed, and the app's executables, Claude SDK CLI, and other helpers retain library validation. This exception does not change the app's development override or the Cloud Worker's refusal of that override.

Signing uses /usr/bin/python3 to scan file headers and batches candidates through /usr/bin/file. Apple's /usr/bin/otool then checks native headers for every architecture, including fat64 containers that file reports as data. Mixed signing categories and malformed native headers stop signing. Java classes, static archives, and Mach-O linker, debug, and core artifacts are resource-sealed rather than directly signed. Discovery opens directories without following symlinks and classifies opened file descriptors rather than redirectable paths. It does not run the bundled Bun to sign itself. Before publishing, the scan checks every observed input, including directory namespaces and resources omitted from the native list. It rejects incomplete traversal and observed changes. This is not an atomic filesystem snapshot.

The signature audit also requires a native Mach-O signature. Some Apple toolchains return success while signing raw fat64 as generic data, without native entitlements. The audit rejects that result. The signer does not thin or convert input containers. Rebuild unsupported native payloads as codesign-compatible code before packaging rather than relying on generic-signature verification.

The signer seals App and Sparkle bundle owners once, after their nested code. A separate signature on a bundle's main executable would also reseal that bundle's resources. After the signer seals the app, a fresh inventory feeds the Team ID and elevation audits. Scanner or classifier failures stop signing. Runtime portability checks inspect each native architecture and reject nonportable dependencies and broken or escaping symlinks.

Sign a private staged copy that no other process modifies. The signer rejects hardlinked files and special files such as FIFOs before attribute cleanup. Copy or rebuild the bundle if this check fails. Attribute cleanup and signing use macOS /usr/bin/sandbox-exec. It restricts filesystem writes to the fixed app and temporary signing directories. Extra inherited file descriptors are closed. Signing fails if this mutation boundary cannot start. There is no unrestricted fallback.

Usage

# from repo root
scripts/package-mac-app.sh                                                      # auto-selects identity; errors if none found
SIGN_IDENTITY="Developer ID Application: Your Name" scripts/package-mac-app.sh   # real cert
ALLOW_ADHOC_SIGNING=1 scripts/package-mac-app.sh                                 # ad-hoc (permissions will not stick)
SIGN_IDENTITY="-" scripts/package-mac-app.sh                                     # explicit ad-hoc (same caveat)
DISABLE_LIBRARY_VALIDATION=1 scripts/package-mac-app.sh                          # dev-only Sparkle Team ID mismatch workaround

Runtime packaging honors the existing APUS_DOCKER_PACKAGE_INVENTORY_TIMEOUT_MS, APUS_DOCKER_PACKAGE_PACK_TIMEOUT_MS, and APUS_DOCKER_PACKAGE_TARBALL_CHECK_TIMEOUT_MS budgets. Inventory and tarball checks default to five minutes; package creation defaults to fifteen minutes. Set a positive integer in milliseconds when packaging needs more time. These budgets pass through the runtime's isolated environment without exposing operator credentials or state. The tarball-check budget covers the whole check, including extraction and validation.

The DISABLE_LIBRARY_VALIDATION=1 development override applies to the app's Sparkle workaround, not its private runtime. ApusCloudWorker.app rejects that override entirely; its elevation-host app executables always retain library validation. The private Bun runtime's plugin-loading exception applies independently of that development flag.

Ad-hoc signing note

SIGN_IDENTITY="-" disables the Hardened Runtime (--options runtime). This prevents crashes when the app loads embedded frameworks (like Sparkle) that do not share the same Team ID. Ad-hoc signatures also break TCC permission persistence. See macOS permissions for recovery steps.

Build metadata for About

Choose About Apus to select About in the native Connection window. It shows the app version and build number, build date, and shortened git commit from Info.plist, even without a Gateway connection. Hover over the commit or date for the full value, or choose Copy Build Info to copy all build metadata. Missing or invalid timestamp and commit values appear as unavailable. Re-run the packager after code changes to refresh these values. App update controls live under Dashboard → Settings → Updates → This Mac.

Related