phase8: Developer ID signing + notarization — spctl 'Notarized Developer ID' on first submission, stapled app + DMG, minimal entitlements; Definition of Done review complete
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 3 changed files with +115 and −5
modified
Makefile
+55 −4
@@ -30,7 +30,7 @@ BUILD_DIR := .build | ||
| 30 | 30 | CONFIG_TITLE = $(shell echo $(CONFIG) | awk '{print toupper(substr($$0,1,1)) substr($$0,2)}') |
| 31 | 31 | BIN_DIR = $(BUILD_DIR)/out/Products/$(CONFIG_TITLE) |
| 32 | 32 | |
| 33 | −.PHONY: build build-xcodebuild app dev run icon clean release check-toolchain | |
| 33 | +.PHONY: build build-xcodebuild app dev run icon icon-if-missing clean release dmg check-toolchain | |
| 34 | 34 | |
| 35 | 35 | check-toolchain: |
| 36 | 36 | @test -d $(XCODE_DEV_DIR) || { echo "error: Xcode toolchain not found at /Applications/Xcode.app — see docs/BUILD.md §2"; exit 1; } |
@@ -70,9 +70,60 @@ icon: | ||
| 70 | 70 | @# Phase 5: SVG → PNGs → .iconset → .icns (rsvg-convert + iconutil) |
| 71 | 71 | bash scripts/make-icon.sh |
| 72 | 72 | |
| 73 | −release: app | |
| 74 | − @# Phase 8: Developer ID signing + notarization + stapling (docs/BUILD.md §4) | |
| 75 | − @echo "release target completed in Phase 8 — currently ad-hoc only"; exit 1 | |
| 73 | +# Phase 8: Developer ID signing + notarization + stapling (docs/BUILD.md §4). | |
| 74 | +# Identity/profile reused from the zyquo-term pipeline (charter Phase 8). | |
| 75 | +IDENTITY := Developer ID Application: Simon-Pierre Boucher (3YM54G49SN) | |
| 76 | +KEYCHAIN_PROFILE := MacLustr-Notarize | |
| 77 | +ENTITLEMENTS := Support/entitlements.plist | |
| 78 | +ZIP := $(DIST)/ZyquoMLX.zip | |
| 79 | +DMG := $(DIST)/ZyquoMLX.dmg | |
| 80 | + | |
| 81 | +release: build icon-if-missing | |
| 82 | + rm -rf "$(APP)" | |
| 83 | + mkdir -p "$(APP)/Contents/MacOS" "$(APP)/Contents/Resources" | |
| 84 | + cp Support/Info.plist "$(APP)/Contents/Info.plist" | |
| 85 | + cp "$(BIN_DIR)/$(EXEC_NAME)" "$(APP)/Contents/MacOS/$(EXEC_NAME)" | |
| 86 | + @for b in "$(BIN_DIR)"/*.bundle; do \ | |
| 87 | + [ -e "$$b" ] && cp -R "$$b" "$(APP)/Contents/Resources/" && echo "bundled $$(basename $$b)"; \ | |
| 88 | + done; true | |
| 89 | + cp assets/icon/AppIcon.icns "$(APP)/Contents/Resources/AppIcon.icns" | |
| 90 | + @echo "=== Signing (Developer ID, hardened runtime, nested-first) ===" | |
| 91 | + @# Resource bundles carry no Mach-O code (metallib + .py are data) but we | |
| 92 | + @# sign any nested code defensively before the outer bundle. | |
| 93 | + @find "$(APP)/Contents/Resources" -type f \( -name "*.dylib" -o -name "*.so" \) -exec \ | |
| 94 | + codesign --force --options runtime --timestamp --sign "$(IDENTITY)" {} \; 2>/dev/null; true | |
| 95 | + codesign --force --options runtime --timestamp \ | |
| 96 | + --entitlements $(ENTITLEMENTS) \ | |
| 97 | + --sign "$(IDENTITY)" "$(APP)/Contents/MacOS/$(EXEC_NAME)" | |
| 98 | + codesign --force --options runtime --timestamp \ | |
| 99 | + --entitlements $(ENTITLEMENTS) \ | |
| 100 | + --sign "$(IDENTITY)" "$(APP)" | |
| 101 | + codesign --verify --deep --strict --verbose=2 "$(APP)" | |
| 102 | + @echo "=== Notarizing (profile: $(KEYCHAIN_PROFILE)) ===" | |
| 103 | + rm -f "$(ZIP)" | |
| 104 | + ditto -c -k --keepParent "$(APP)" "$(ZIP)" | |
| 105 | + xcrun notarytool submit "$(ZIP)" --keychain-profile "$(KEYCHAIN_PROFILE)" --wait | |
| 106 | + xcrun stapler staple "$(APP)" | |
| 107 | + @echo "=== Verifying ===" | |
| 108 | + spctl -a -vv "$(APP)" | |
| 109 | + xcrun stapler validate "$(APP)" | |
| 110 | + @echo "→ $(APP) (Developer ID signed, notarized, stapled)" | |
| 111 | + | |
| 112 | +dmg: | |
| 113 | + rm -f "$(DMG)" | |
| 114 | + rm -rf $(DIST)/dmg_temp | |
| 115 | + mkdir -p $(DIST)/dmg_temp | |
| 116 | + cp -R "$(APP)" $(DIST)/dmg_temp/ | |
| 117 | + ln -s /Applications "$(DIST)/dmg_temp/Applications" | |
| 118 | + hdiutil create -volname "$(APP_NAME)" -srcfolder $(DIST)/dmg_temp -ov -format UDZO "$(DMG)" | |
| 119 | + rm -rf $(DIST)/dmg_temp | |
| 120 | + codesign --force --sign "$(IDENTITY)" --timestamp "$(DMG)" | |
| 121 | + xcrun notarytool submit "$(DMG)" --keychain-profile "$(KEYCHAIN_PROFILE)" --wait | |
| 122 | + xcrun stapler staple "$(DMG)" | |
| 123 | + @echo "→ $(DMG) (signed, notarized, stapled)" | |
| 124 | + | |
| 125 | +icon-if-missing: | |
| 126 | + @test -f assets/icon/AppIcon.icns || bash scripts/make-icon.sh | |
| 76 | 127 | |
| 77 | 128 | clean: |
| 78 | 129 | rm -rf $(BUILD_DIR) $(DIST) |
added
Support/entitlements.plist
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!-- | |
| 3 | + entitlements.plist | |
| 4 | + Zyquo MLX | |
| 5 | + | |
| 6 | + Author: Simon-Pierre Boucher | |
| 7 | + Mail: contact@spboucher.ai | |
| 8 | + | |
| 9 | + Minimal posture per docs/BUILD.md §4: Hardened Runtime is enabled at signing | |
| 10 | + time; MLX's runtime kernel compilation goes through the Metal framework | |
| 11 | + (GPU shader compile), NOT process-writable-executable memory, so no | |
| 12 | + allow-jit / unsigned-executable-memory is needed (verified: notarization | |
| 13 | + passes without them). The app is a local ML workbench needing broad | |
| 14 | + file/compute access and spawns its own provisioned Python — no App Sandbox | |
| 15 | + (same rationale as the rest of the Zyquo family). | |
| 16 | +--> | |
| 17 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 18 | +<plist version="1.0"> | |
| 19 | +<dict> | |
| 20 | + <key>com.apple.security.cs.allow-jit</key> | |
| 21 | + <false/> | |
| 22 | +</dict> | |
| 23 | +</plist> | |
modified
docs/PLAN.md
+37 −1
@@ -177,4 +177,40 @@ model types plus training/convert/hub/python-env. Three real-world defects | ||
| 177 | 177 | were found *by* verification and fixed: cancelled runs mis-recorded as |
| 178 | 178 | completed, decoder-style embedding repos mis-typed as LLM, and stale sharded |
| 179 | 179 | indexes in upstream repos rejected by validation. Results in VERIFICATION.md. |
| 180 | −## Phase 8 — Signing & Notarization (not started) | |
| 180 | +## Phase 8 — Signing & Notarization ✅ (completed 2026-07-30) | |
| 181 | + | |
| 182 | +- [x] zyquo-term setup inspected: identity `Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)` + profile `MacLustr-Notarize` reused; identity verified present in keychain (no secrets printed/committed) | |
| 183 | +- [x] `Support/entitlements.plist` — Hardened Runtime, minimal posture (no sandbox, no allow-jit — MLX's Metal runtime compile is GPU shader compilation, not process JIT) | |
| 184 | +- [x] `make release`: nested-first signing → `ditto` zip → `notarytool submit --wait` → staple → verify | |
| 185 | +- [x] Signed + notarized + stapled DMG (`make dmg`) | |
| 186 | +- [x] **PHASE GATE GREEN on first submission:** `spctl -a -vv` → `accepted, source=Notarized Developer ID`; `stapler validate` OK; notarized app launches | |
| 187 | +- [x] Final coherence sweep: headers ✓, naming ✓, no TODO/dead code ✓, zero warnings ✓ | |
| 188 | + | |
| 189 | +**Phase 8 summary:** `make release` produces a Developer ID-signed, notarized, | |
| 190 | +stapled `Zyquo MLX.app` (plus DMG) entirely from the command line — no Xcode | |
| 191 | +IDE, no `.xcodeproj`. Notarization passed on the first attempt with the | |
| 192 | +minimal entitlements posture, confirming BUILD.md §4's analysis. The | |
| 193 | +bundled-Python notarization minefield never arises because the venv lives in | |
| 194 | +Application Support, outside the signed bundle. | |
| 195 | + | |
| 196 | +--- | |
| 197 | + | |
| 198 | +## Definition of Done — final review (2026-07-30) | |
| 199 | + | |
| 200 | +- ✅ `make release` → Developer ID-signed, notarized, stapled app, no Xcode IDE | |
| 201 | +- ✅ Inference across MLX-supported types: LLM/VLM/embeddings/speech verified | |
| 202 | + with streaming + memory release (image-gen: documented deferral — upstream | |
| 203 | + pipelines are script-only; Swift StableDiffusion lib is the follow-up path) | |
| 204 | +- ✅ Real LoRA + QLoRA end-to-end: live metrics, checkpoints, cancel, warm | |
| 205 | + resume, fuse, export — with two upstream landmines found and handled | |
| 206 | +- ✅ Conversion + quantization produce valid, running MLX models (Swift-native | |
| 207 | + + Python paths); export lands in the shared Models library | |
| 208 | +- ✅ Datasets import/validate/split/preview; Hub browse + resumable downloads + | |
| 209 | + curated catalog live-verified (16/16 + 43-repo sweep) | |
| 210 | +- ✅ MemoryAdvisor: device-derived gating, verdicts on every surface, size | |
| 211 | + prediction measured at 0.1 % accuracy; blocked-config UX verified | |
| 212 | +- ✅ Copper-on-slate icon (Z dominant, anvil/spark story), SVG source of truth, | |
| 213 | + Dock-verified; light theme flagship + derived dark, token-only styling | |
| 214 | +- ✅ Naming coherent (`Zyquo MLX` / `com.zyquo.mlx` / `ZyquoMLX`); headers on | |
| 215 | + every code file (swept); zero warnings; phase-prefixed commit history | |
| 216 | +- ✅ All Phase 0 research docs complete and traceable into the implementation | |
| 181 | 217 | |