phase8: Developer ID signing + notarization — notarize.sh (inside-out sign, notarytool --wait, staple, spctl), universal single-command build, Hardened Runtime entitlements; make release → Notarized Developer ID (verified). All 9 phases complete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 6 changed files with +117 and −29
modified
Makefile
+7 −15
@@ -59,28 +59,20 @@ bundle: | ||
| 59 | 59 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done |
| 60 | 60 | scripts/write-info-plist.sh "$(APP_DIR)" "$(APP_NAME)" "$(EXEC_NAME)" "$(BUNDLE_ID)" "$(VERSION)" "$(BUILD_NUM)" "$(MIN_MACOS)" |
| 61 | 61 | |
| 62 | −# Both arch builds land in .build/out/Products/Release, so each slice is copied | |
| 63 | −# aside before lipo. The -U flag allows the swiftCompatibility56 force-load | |
| 64 | −# symbol (CLT ships it arm64-only; never referenced at runtime on macOS 13+). | |
| 62 | +# SwiftPM builds a fat binary in one command (arm64 + x86_64); products land in | |
| 63 | +# .build/apple/Products/Release. Verifies the slices with lipo. | |
| 64 | +PRODUCTS := .build/apple/Products/Release | |
| 65 | 65 | universal: |
| 66 | 66 | @echo "=== Building universal binary (arm64 + x86_64) ===" |
| 67 | − mkdir -p .build/universal | |
| 68 | − swift build -c release --arch arm64 | |
| 69 | − cp .build/out/Products/Release/$(EXEC_NAME) .build/universal/$(EXEC_NAME).arm64 | |
| 70 | − swift build -c release --arch x86_64 -Xlinker -U -Xlinker '__swift_FORCE_LOAD_$$_swiftCompatibility56' | |
| 71 | − cp .build/out/Products/Release/$(EXEC_NAME) .build/universal/$(EXEC_NAME).x86_64 | |
| 72 | − lipo -create \ | |
| 73 | − .build/universal/$(EXEC_NAME).arm64 \ | |
| 74 | − .build/universal/$(EXEC_NAME).x86_64 \ | |
| 75 | − -output .build/universal/$(EXEC_NAME) | |
| 76 | − lipo -archs .build/universal/$(EXEC_NAME) | |
| 67 | + swift build -c release --arch arm64 --arch x86_64 | |
| 68 | + lipo -archs "$(PRODUCTS)/$(EXEC_NAME)" | |
| 77 | 69 | |
| 78 | 70 | release: universal |
| 79 | 71 | @echo "=== Assembling $(APP_DIR) (universal) ===" |
| 80 | 72 | rm -rf "$(APP_DIR)" |
| 81 | 73 | mkdir -p "$(APP_DIR)/Contents/MacOS" "$(APP_DIR)/Contents/Resources" |
| 82 | − cp .build/universal/$(EXEC_NAME) "$(APP_DIR)/Contents/MacOS/$(EXEC_NAME)" | |
| 83 | − @for b in .build/out/Products/Release/*.bundle; do [ -e "$$b" ] && cp -R "$$b" "$(APP_DIR)/Contents/Resources/" || true; done | |
| 74 | + cp "$(PRODUCTS)/$(EXEC_NAME)" "$(APP_DIR)/Contents/MacOS/$(EXEC_NAME)" | |
| 75 | + @for b in "$(PRODUCTS)"/*.bundle; do [ -e "$$b" ] && cp -R "$$b" "$(APP_DIR)/Contents/Resources/" || true; done | |
| 84 | 76 | @if [ -f Resources/AppIcon.icns ]; then cp Resources/AppIcon.icns "$(APP_DIR)/Contents/Resources/AppIcon.icns"; fi |
| 85 | 77 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done |
| 86 | 78 | scripts/write-info-plist.sh "$(APP_DIR)" "$(APP_NAME)" "$(EXEC_NAME)" "$(BUNDLE_ID)" "$(VERSION)" "$(BUILD_NUM)" "$(MIN_MACOS)" |
modified
README.md
+9 −3
@@ -18,12 +18,18 @@ surface, powered by the same providers/models and encrypted key vault as | ||
| 18 | 18 | ```sh |
| 19 | 19 | make dev # release build → dist/Zyquo Atlas.app, ad-hoc signed (local iteration) |
| 20 | 20 | make run # make dev, then launch the app |
| 21 | −make test # run the test suite (Command Line Tools only) | |
| 22 | −make release # universal, Developer ID signed, notarized, stapled (Phase 8) | |
| 23 | −make verify # Phase 7 API verification with real keys (source .env.keys) | |
| 21 | +make test # run the test suite | |
| 22 | +make release # universal (arm64+x86_64), Developer ID signed, notarized, stapled | |
| 23 | +make icon # regenerate AppIcon.icns from assets/icon/zyquo-atlas.svg | |
| 24 | +make verify # API verification with real keys (source .env.keys) | |
| 24 | 25 | make clean |
| 25 | 26 | ``` |
| 26 | 27 | |
| 28 | +`make release` produces a **Notarized Developer ID** `Zyquo Atlas.app` (verified: | |
| 29 | +`spctl -a -vv` → `source=Notarized Developer ID`), universal, built without the | |
| 30 | +Xcode IDE. Signing identity, Team ID, and the `notarytool` keychain profile are | |
| 31 | +reused from the Zyquo family pipeline; no secrets live in this repo. | |
| 32 | + | |
| 27 | 33 | ### Toolchain note |
| 28 | 34 | |
| 29 | 35 | The Command Line Tools' default SDK (macOS 27 beta) declares SwiftUI's `@State` |
modified
Resources/ZyquoAtlas.entitlements
+10 −7
@@ -7,16 +7,19 @@ | ||
| 7 | 7 | Author: Simon-Pierre Boucher |
| 8 | 8 | Mail: contact@spboucher.ai |
| 9 | 9 | |
| 10 | − Phase 8 finalizes the Hardened Runtime posture. A WKWebView browser uses | |
| 11 | − WebKit XPC/child processes; the minimal set that lets WKWebView, downloads, | |
| 12 | − and the app's own HTTPS API calls work is chosen and validated in Phase 8. | |
| 13 | − Not App-Sandboxed in v1 (Developer ID distribution, not Mac App Store). | |
| 10 | + Signed with the Hardened Runtime (Developer ID distribution). Zyquo Atlas is | |
| 11 | + a WKWebView browser: WebKit runs page JavaScript in Apple's own out-of-process | |
| 12 | + WebContent/Networking/GPU XPC services (WebKit.framework is a *system* | |
| 13 | + framework, not embedded), so the host app needs no JIT or network | |
| 14 | + entitlement for WKWebView, downloads, or its own HTTPS API calls to function. | |
| 15 | + Not App-Sandboxed (like Safari-alternative browsers and terminals); a browser | |
| 16 | + loads arbitrary content and manages its own data outside a container. This is | |
| 17 | + the minimal posture that notarizes and runs — matching the Zyquo family | |
| 18 | + pipeline (zyquo-term). | |
| 14 | 19 | --> |
| 15 | 20 | <plist version="1.0"> |
| 16 | 21 | <dict> |
| 17 | 22 | <key>com.apple.security.cs.allow-jit</key> |
| 18 | − <true/> | |
| 19 | − <key>com.apple.security.network.client</key> | |
| 20 | − <true/> | |
| 23 | + <false/> | |
| 21 | 24 | </dict> |
| 22 | 25 | </plist> |
modified
docs/PLAN.md
+29 −0
@@ -165,3 +165,32 @@ launchable, single window; browser core is Phase 2). | ||
| 165 | 165 | **Failures found & fixed:** first sweep 173/184 → three genuine failures fixed — two reasoning models (Together `Qwen3.5-9B`, DeepInfra `Kimi-K2.5`) were unflagged in the catalog and exhausted the token budget on hidden thinking → flagged `reasoning` + raised the reasoning token budget (harness 8000, AIService 8192, a real product fix so reasoning models reach their answer); one non-functional model (Together `google/gemma-4-31B-it`, streams empty) removed from the catalog (now **169 models**). Remaining failures were **transient provider-side network errors** (Cerebras/DeepInfra/Together 503/timeout during concurrent bursts) that all cleared on retry — recorded in `docs/VERIFICATION.md` in their re-verified green state. |
| 166 | 166 | |
| 167 | 167 | **Final: 183 checks · 183 passed · 0 failed.** No keys or browsing data committed. **Phase 7 gate PASSED.** |
| 168 | + | |
| 169 | +## Phase 8 — Signing & Notarization — COMPLETE (Notarized Developer ID) | |
| 170 | + | |
| 171 | +- [x] Inspected `zyquo-term` signing setup and reused it verbatim: identity `Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)`, Team `3YM54G49SN`, notarytool keychain profile `MacLustr-Notarize` (both confirmed present/live on this machine) | |
| 172 | +- [x] `Resources/ZyquoAtlas.entitlements` — Hardened Runtime posture; **no special entitlements needed** because WKWebView runs page JS in Apple's system WebContent XPC processes (WebKit.framework is not embedded); not App-Sandboxed (browser). Minimal, matches the family. | |
| 173 | +- [x] `scripts/notarize.sh` — signs inside-out (nested SPM resource bundle, then the app) with `--options runtime --timestamp --entitlements`, `ditto -c -k --keepParent` zip, `notarytool submit --wait`, `stapler staple` + `validate`, `spctl` assessment | |
| 174 | +- [x] Makefile `universal` fixed to SwiftPM single-command multi-arch (`swift build -c release --arch arm64 --arch x86_64` → fat binary in `.build/apple/Products/Release`; the old two-build+lipo produced two arm64 slices under the now-installed Xcode 26.6 toolchain) | |
| 175 | +- [x] `make release` run end-to-end: **Apple notarization status: Accepted** (submission `2003be13-…`), stapled + validated | |
| 176 | + | |
| 177 | +**Verified independently:** `lipo -archs` → `x86_64 arm64`; `codesign -dvv` → `flags=0x10000(runtime)`, Authority `Developer ID Application …`, `TeamIdentifier=3YM54G49SN`; `stapler validate` → worked; **`spctl -a -vv` → `accepted, source=Notarized Developer ID`**. **Phase 8 gate PASSED.** | |
| 178 | + | |
| 179 | +Notes: `make dev` (ad-hoc) retained for iteration. Optional signed+stapled DMG is an easy follow-up (the app is already notarized+stapled). Vendored `Content/Readability*.js` (Mozilla, Apache-2.0, unmodified) keep Mozilla's copyright header rather than the Atlas author header — correct for license integrity; attribution in `Content/LICENSE-Readability.txt`. | |
| 180 | + | |
| 181 | +--- | |
| 182 | + | |
| 183 | +## Definition of Done — final check (2026-07-30) | |
| 184 | + | |
| 185 | +- [x] `make release` → Developer ID–signed, **notarized, stapled** `Zyquo Atlas.app`, universal, built without the Xcode IDE (SPM/CLI); `spctl` = Notarized Developer ID | |
| 186 | +- [x] Fast multi-tab WKWebView browser: profiles, private mode, favorites, full-text history, downloads, find, reader | |
| 187 | +- [x] AI everywhere: omnibox ask, chat-with-page, selection actions, summarize/translate/rewrite, multi-tab compare — streaming, grounded, every provider/model (Phase 7 table green, 169 models) | |
| 188 | +- [x] Theming/customization engine: themes, backgrounds, top/left tabs, density, custom start page — live, per profile | |
| 189 | +- [x] Privacy model: page content leaves only on user action; keys in the reused AES-256-GCM vault (no Keychain) | |
| 190 | +- [x] Teal-indigo "atlas" SVG icon → `.icns` + template glyph; sibling of Cloud/Local/Agent | |
| 191 | +- [x] Light theme matches the Phase 4 spec; dark derived | |
| 192 | +- [x] Naming coherent (`Zyquo Atlas`, `com.zyquo.atlas`, `ZyquoAtlas`); every authored code file carries the header (verified) | |
| 193 | +- [x] `docs/PLAN.md` all phases complete; `AI-BROWSER-RESEARCH.md`, `PROVIDER-REUSE.md`, `VERIFICATION.md` complete | |
| 194 | +- [~] Deferred enhancements documented in Phase 6 (tab groups/drag-reorder gesture, hover thumbnails, in-field writing-assist, auto/hover summaries, citation-chip rendering, menu-bar extra) | |
| 195 | + | |
| 196 | +**All 9 phases (0–8) complete. Zyquo Atlas builds, verifies green, and ships as a notarized Developer ID app.** | |
modified
docs/VERIFICATION.md
+4 −4
@@ -18,8 +18,8 @@ come from the environment and are never logged. | ||
| 18 | 18 | |
| 19 | 19 | | Source | Page | Test | Result | Latency | Detail | |
| 20 | 20 | |---|---|---|---|---|---| |
| 21 | −| — | `article` | extract | ✅ | 2.3s | quality=reader, words=9210, ~17278tok, headings=27 | | |
| 21 | +| — | `article` | extract | ✅ | 1.9s | quality=reader, words=9210, ~17278tok, headings=27 | | |
| 22 | 22 | | — | `docs` | extract | ✅ | 1.7s | quality=reader, words=83, ~190tok, headings=0 | |
| 23 | −| — | `js-app` | extract | ✅ | 1.9s | quality=reader, words=1174, ~2830tok, headings=23 | | |
| 24 | −| — | `long-article` | extract | ✅ | 1.9s | quality=reader, words=28685, ~49595tok, headings=40 | | |
| 25 | −| — | `selection` | extract | ✅ | 2.4s | selection=In cartography, a map is a two-dimension, words=18106 | | |
| 23 | +| — | `js-app` | extract | ✅ | 1.8s | quality=reader, words=1174, ~2830tok, headings=23 | | |
| 24 | +| — | `long-article` | extract | ✅ | 1.8s | quality=reader, words=28685, ~49595tok, headings=40 | | |
| 25 | +| — | `selection` | extract | ✅ | 2.1s | selection=In cartography, a map is a two-dimension, words=18106 | | |
added
scripts/notarize.sh
+58 −0
@@ -0,0 +1,58 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# | |
| 3 | +# notarize.sh | |
| 4 | +# Zyquo Atlas | |
| 5 | +# | |
| 6 | +# Author: Simon-Pierre Boucher | |
| 7 | +# Mail: contact@spboucher.ai | |
| 8 | +# | |
| 9 | +# Developer ID signing + notarization + stapling for an assembled | |
| 10 | +# Zyquo Atlas.app (Phase 8). Identity, Team ID, and the notarytool keychain | |
| 11 | +# profile are reused from the Zyquo family pipeline (zyquo-term). | |
| 12 | +# | |
| 13 | +# Usage: notarize.sh <app-dir> <identity> <notary-profile> <entitlements> | |
| 14 | +# Signs inside-out (nested resource bundles, then the app) with the Hardened | |
| 15 | +# Runtime, zips with ditto, submits with notarytool --wait, staples, and | |
| 16 | +# verifies spctl == "Notarized Developer ID". Secrets are never printed. | |
| 17 | +# | |
| 18 | +set -euo pipefail | |
| 19 | +cd "$(dirname "$0")/.." | |
| 20 | + | |
| 21 | +APP_DIR="$1" | |
| 22 | +IDENTITY="$2" | |
| 23 | +KEYCHAIN_PROFILE="$3" | |
| 24 | +ENTITLEMENTS="$4" | |
| 25 | +ZIP_PATH="${APP_DIR%.app}.zip" | |
| 26 | + | |
| 27 | +[ -d "$APP_DIR" ] || { echo "ERROR: $APP_DIR missing — run 'make release' (assembles the bundle first)" >&2; exit 1; } | |
| 28 | + | |
| 29 | +echo "=== Signing (Developer ID, Hardened Runtime) ===" | |
| 30 | +# Sign nested code first (SPM resource bundles carrying the Content/*.js and | |
| 31 | +# icon), inside-out, so the outer signature stays valid. | |
| 32 | +find "$APP_DIR/Contents" -name "*.bundle" -maxdepth 3 -print0 2>/dev/null \ | |
| 33 | + | while IFS= read -r -d '' bundle; do | |
| 34 | + echo " nested: $(basename "$bundle")" | |
| 35 | + codesign --force --options runtime --timestamp \ | |
| 36 | + --entitlements "$ENTITLEMENTS" --sign "$IDENTITY" "$bundle" | |
| 37 | + done | |
| 38 | +# Sign the app bundle (also signs Contents/MacOS/ZyquoAtlas). | |
| 39 | +codesign --force --options runtime --timestamp \ | |
| 40 | + --entitlements "$ENTITLEMENTS" --sign "$IDENTITY" "$APP_DIR" | |
| 41 | +codesign --verify --deep --strict --verbose=2 "$APP_DIR" | |
| 42 | +echo "Signature valid." | |
| 43 | + | |
| 44 | +echo "=== Packaging for notarization (ditto) ===" | |
| 45 | +rm -f "$ZIP_PATH" | |
| 46 | +ditto -c -k --keepParent "$APP_DIR" "$ZIP_PATH" | |
| 47 | + | |
| 48 | +echo "=== Submitting to notarytool (profile: $KEYCHAIN_PROFILE) ===" | |
| 49 | +xcrun notarytool submit "$ZIP_PATH" --keychain-profile "$KEYCHAIN_PROFILE" --wait | |
| 50 | + | |
| 51 | +echo "=== Stapling ===" | |
| 52 | +xcrun stapler staple "$APP_DIR" | |
| 53 | +xcrun stapler validate "$APP_DIR" | |
| 54 | + | |
| 55 | +echo "=== Gatekeeper assessment ===" | |
| 56 | +spctl -a -vvv --type execute "$APP_DIR" | |
| 57 | + | |
| 58 | +echo "Done: $APP_DIR is Developer ID–signed, notarized, and stapled." | |
| 59 | ||