From c9492c90ea47bdd34d7955a6c9b11dba69f51569 Mon Sep 17 00:00:00 2001 From: George Brancovici Date: Wed, 13 May 2026 12:18:56 -0400 Subject: [PATCH 1/4] Add code signing, notarization, and DMG packaging scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables distributing Galaxy.app to end users without Gatekeeper warnings. Uses Samsung Developer ID Application certificate with hardened runtime. Pipeline: cargo bundle → sign.sh → notarize.sh → package.sh → Galaxy.dmg Entitlements: - network.client (AWS Bedrock API calls) - automation.apple-events (osascript for CLI install) - cs.allow-unsigned-executable-memory (Metal shader compilation) Orchestration scripts: - build-debug.sh: full pipeline with debug build - build-release.sh: full pipeline with release build Co-Authored-By: Claude Opus 4.6 (1M context) --- BuildSupport/Galaxy.entitlements | 13 ++++++ build-debug.sh | 14 +++++++ build-release.sh | 14 +++++++ notarize.sh | 49 +++++++++++++++++++++++ package.sh | 69 ++++++++++++++++++++++++++++++++ sign.sh | 56 ++++++++++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 BuildSupport/Galaxy.entitlements create mode 100755 build-debug.sh create mode 100755 build-release.sh create mode 100755 notarize.sh create mode 100755 package.sh create mode 100755 sign.sh diff --git a/BuildSupport/Galaxy.entitlements b/BuildSupport/Galaxy.entitlements new file mode 100644 index 00000000..5aada797 --- /dev/null +++ b/BuildSupport/Galaxy.entitlements @@ -0,0 +1,13 @@ + + + + + com.apple.security.network.client + + com.apple.security.automation.apple-events + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/build-debug.sh b/build-debug.sh new file mode 100755 index 00000000..ce414e09 --- /dev/null +++ b/build-debug.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "=== Galaxy Debug Build + Sign + Notarize + Package ===" +echo "" + +cargo bundle --bin galaxy-oss --package galaxy +./sign.sh +./notarize.sh +./package.sh + +echo "" +echo "=== Done. Distributable DMG: target/debug/bundle/osx/Galaxy.dmg ===" diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 00000000..04d3079e --- /dev/null +++ b/build-release.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "=== Galaxy Release Build + Sign + Notarize + Package ===" +echo "" + +cargo bundle --release --bin galaxy-oss --package galaxy +./sign.sh --release +./notarize.sh --release +./package.sh --release + +echo "" +echo "=== Done. Distributable DMG: target/release/bundle/osx/Galaxy.dmg ===" diff --git a/notarize.sh b/notarize.sh new file mode 100755 index 00000000..cf748387 --- /dev/null +++ b/notarize.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" + ZIP_PATH="target/release/bundle/osx/Galaxy.zip" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" + ZIP_PATH="target/debug/bundle/osx/Galaxy.zip" +fi + +trap 'rm -f "$ZIP_PATH"' EXIT + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + exit 1 +fi + +echo "Verifying app is signed before notarization..." +codesign --verify --strict "$APP_PATH" || { + echo "Error: App is not properly signed. Run ./sign.sh first." + exit 1 +} + +echo "Creating zip for notarization submission..." +rm -f "$ZIP_PATH" +ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + +echo "Submitting to Apple notary service (this may take a few minutes)..." +RESULT=$(xcrun notarytool submit "$ZIP_PATH" \ + --keychain-profile notarytool-profile \ + --wait 2>&1) +echo "$RESULT" + +if ! echo "$RESULT" | grep -q "status: Accepted"; then + echo "" + echo "Error: Notarization was not accepted. Check output above." + exit 1 +fi + +echo "Stapling notarization ticket to app..." +xcrun stapler staple "$APP_PATH" + +echo "Validating stapled ticket..." +xcrun stapler validate "$APP_PATH" + +echo "" +echo "Done. Galaxy.app is notarized and stapled." diff --git a/package.sh b/package.sh new file mode 100755 index 00000000..d1cd8abc --- /dev/null +++ b/package.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +SIGNING_IDENTITY="Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" + DMG_PATH="target/release/bundle/osx/Galaxy.dmg" + STAGING_DIR="target/release/bundle/osx/dmg-staging" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" + DMG_PATH="target/debug/bundle/osx/Galaxy.dmg" + STAGING_DIR="target/debug/bundle/osx/dmg-staging" +fi + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + exit 1 +fi + +echo "Verifying app is notarized..." +xcrun stapler validate "$APP_PATH" || { + echo "Error: App does not have a valid notarization ticket. Run ./sign.sh and ./notarize.sh first." + exit 1 +} + +echo "Preparing DMG staging directory..." +rm -rf "$STAGING_DIR" +mkdir -p "$STAGING_DIR" +cp -R "$APP_PATH" "$STAGING_DIR/" +ln -s /Applications "$STAGING_DIR/Applications" + +echo "Creating DMG..." +hdiutil detach /Volumes/Galaxy -force 2>/dev/null || true +rm -f "$DMG_PATH" +hdiutil create -volname "Galaxy" \ + -srcfolder "$STAGING_DIR" \ + -ov -format UDZO \ + "$DMG_PATH" + +echo "Signing DMG..." +codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$DMG_PATH" + +echo "Submitting DMG to Apple notary service..." +RESULT=$(xcrun notarytool submit "$DMG_PATH" \ + --keychain-profile notarytool-profile \ + --wait 2>&1) +echo "$RESULT" + +if ! echo "$RESULT" | grep -q "status: Accepted"; then + echo "" + echo "Error: DMG notarization was not accepted. Check output above." + exit 1 +fi + +echo "Stapling notarization ticket to DMG..." +xcrun stapler staple "$DMG_PATH" + +echo "Cleaning up staging directory..." +rm -rf "$STAGING_DIR" + +echo "" +echo "Verifying final DMG..." +xcrun stapler validate "$DMG_PATH" + +echo "" +echo "Done. Galaxy.dmg is ready for distribution at:" +echo " $DMG_PATH" diff --git a/sign.sh b/sign.sh new file mode 100755 index 00000000..30cb780f --- /dev/null +++ b/sign.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +SIGNING_IDENTITY="Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)" +ENTITLEMENTS="BuildSupport/Galaxy.entitlements" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" +fi + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + echo "Run first: cargo bundle --bin galaxy-oss --package galaxy" + exit 1 +fi + +if [ ! -f "$ENTITLEMENTS" ]; then + echo "Error: $ENTITLEMENTS not found." + exit 1 +fi + +JUNK=$(find "$APP_PATH" -maxdepth 1 -not -name "Contents" -not -path "$APP_PATH" 2>/dev/null || true) +if [ -n "$JUNK" ]; then + echo "Removing unsealed items from bundle root: $JUNK" + echo "$JUNK" | xargs rm -rf +fi + +echo "Removing .DS_Store files..." +find "$APP_PATH" -name '.DS_Store' -delete 2>/dev/null || true + +echo "Stripping extended attributes..." +xattr -cr "$APP_PATH" + +echo "Signing $APP_PATH..." +codesign --force --sign "$SIGNING_IDENTITY" \ + --entitlements "$ENTITLEMENTS" \ + --options runtime \ + --timestamp \ + "$APP_PATH/Contents/MacOS/galaxy-oss" + +codesign --force --sign "$SIGNING_IDENTITY" \ + --entitlements "$ENTITLEMENTS" \ + --options runtime \ + --timestamp \ + "$APP_PATH" + +echo "Verifying signature..." +codesign --verify --strict "$APP_PATH" +codesign -dvvv "$APP_PATH" 2>&1 | grep -E "Authority|TeamIdentifier|Runtime" +echo "Architecture: $(lipo -archs "$APP_PATH/Contents/MacOS/galaxy-oss")" + +echo "" +echo "Done. Galaxy.app is signed and ready for notarization." From 9cd70549ca1a4c6e9995708ee24b58e0e7f5b0bd Mon Sep 17 00:00:00 2001 From: George Brancovici Date: Wed, 13 May 2026 13:21:14 -0400 Subject: [PATCH 2/4] Fix hdiutil Resource Busy: retry loop + Spotlight indexing prevention Adds .metadata_never_index to staging dir and retries hdiutil create up to 3 times to handle transient "Resource busy" failures from Spotlight/fseventsd locking the source folder. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/package.sh b/package.sh index d1cd8abc..f0583931 100755 --- a/package.sh +++ b/package.sh @@ -34,10 +34,23 @@ ln -s /Applications "$STAGING_DIR/Applications" echo "Creating DMG..." hdiutil detach /Volumes/Galaxy -force 2>/dev/null || true rm -f "$DMG_PATH" -hdiutil create -volname "Galaxy" \ - -srcfolder "$STAGING_DIR" \ - -ov -format UDZO \ - "$DMG_PATH" +# Prevent Spotlight/fseventsd from locking the staging dir +touch "$STAGING_DIR/.metadata_never_index" +sleep 1 +for attempt in 1 2 3; do + if hdiutil create -volname "Galaxy" \ + -srcfolder "$STAGING_DIR" \ + -ov -format UDZO \ + "$DMG_PATH"; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Error: hdiutil create failed after 3 attempts" + exit 1 + fi + echo "hdiutil failed (attempt $attempt/3), retrying in 3s..." + sleep 3 +done echo "Signing DMG..." codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$DMG_PATH" From cd52f5f9409906fe26e7debcc392713485108317 Mon Sep 17 00:00:00 2001 From: George Brancovici Date: Thu, 14 May 2026 14:56:54 -0400 Subject: [PATCH 3/4] Add copy-dmgs.sh to copy DMGs to ~/Downloads with date+letter suffix Co-Authored-By: Claude Opus 4.6 (1M context) --- copy-dmgs.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 copy-dmgs.sh diff --git a/copy-dmgs.sh b/copy-dmgs.sh new file mode 100755 index 00000000..0c450fe2 --- /dev/null +++ b/copy-dmgs.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +DATE=$(date +%Y%m%d) +DEST="$HOME/Downloads" + +next_letter() { + local prefix="$1" + local letter="a" + while [ -f "${prefix}${letter}.dmg" ]; do + letter=$(echo "$letter" | tr 'a-y' 'b-z') + if [ "$letter" = "z" ] && [ -f "${prefix}z.dmg" ]; then + echo "Error: ran out of letter indices for today" >&2 + exit 1 + fi + done + echo "$letter" +} + +if [ -f "target/debug/bundle/osx/Galaxy.dmg" ]; then + LETTER=$(next_letter "$DEST/Galaxy-debug-${DATE}") + cp "target/debug/bundle/osx/Galaxy.dmg" "$DEST/Galaxy-debug-${DATE}${LETTER}.dmg" + echo "Copied: $DEST/Galaxy-debug-${DATE}${LETTER}.dmg" +else + echo "Skipped: no debug DMG found" +fi + +if [ -f "target/release/bundle/osx/Galaxy.dmg" ]; then + LETTER=$(next_letter "$DEST/Galaxy-release-${DATE}") + cp "target/release/bundle/osx/Galaxy.dmg" "$DEST/Galaxy-release-${DATE}${LETTER}.dmg" + echo "Copied: $DEST/Galaxy-release-${DATE}${LETTER}.dmg" +else + echo "Skipped: no release DMG found" +fi From ab59eb1ecc5fae8bc8301c38c7faa56be7e249df Mon Sep 17 00:00:00 2001 From: George Brancovici Date: Thu, 14 May 2026 15:00:41 -0400 Subject: [PATCH 4/4] Update GALAXY.md with signing pipeline and diagnostics documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- GALAXY.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/GALAXY.md b/GALAXY.md index dd067a6f..319c319a 100644 --- a/GALAXY.md +++ b/GALAXY.md @@ -165,6 +165,46 @@ When adding/editing match statements, avoid using the wildcard _ when at all pos **pull_warp_feature** (`.warp/skills/pull_warp_feature/SKILL.md`): Ports features from upstream Warp into Galaxy. Clones Warp source into `.galaxy/warp-upstream/` (gitignored), analyzes the feature for Warp-specific dependencies (AI → Bedrock, cloud → local storage, telemetry → removed), presents a compatibility plan, and implements the port after approval. All AI must go through `BedrockClient`; all cloud storage must be local. +### Code Signing & Distribution + +Galaxy uses Samsung's Developer ID Application certificate for macOS code signing and Apple notarization. + +**Signing identity:** `Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)` + +**Entitlements** (`BuildSupport/Galaxy.entitlements`): +- `com.apple.security.network.client` — AWS Bedrock API calls +- `com.apple.security.automation.apple-events` — osascript for CLI install +- `com.apple.security.cs.allow-unsigned-executable-memory` — Metal shader compilation + +**Build & distribute pipeline:** +```bash +# Debug (unoptimized, ~149MB) +./build-debug.sh + +# Release (optimized, smaller) +./build-release.sh + +# Or step by step: +cargo bundle [--release] --bin galaxy-oss --package galaxy +./sign.sh [--release] +./notarize.sh [--release] +./package.sh [--release] + +# Copy DMGs to ~/Downloads with date suffix +./copy-dmgs.sh +``` + +**Output:** `target/{debug|release}/bundle/osx/Galaxy.dmg` — signed, notarized, stapled. Users can download, open, drag to Applications, launch without Gatekeeper warnings. + +**Install from source (end users):** +```bash +./script/install-galaxy.sh +``` + +### Bedrock Diagnostics + +When `GALAXY_BEDROCK_DIAGNOSTICS=1` is set, the diagnostic logger writes to `bedrock-diagnostics.log` in the log directory. On API errors, a comprehensive JSON snapshot (`Error_YYYYMMDD_HHMMSS_mmm.txt`) is written to the repo root (or cwd/tmp) containing the full request context, captured log lines, and log tails. + ## Future Work ### IDE-Level LSP Integration