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>
6 changed files +117 −29
modified
Makefile
+7 −15
@@ -59,28 +59,20 @@ bundle: | |||||
| 59 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done | 59 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done | ||
| 60 | scripts/write-info-plist.sh "$(APP_DIR)" "$(APP_NAME)" "$(EXEC_NAME)" "$(BUNDLE_ID)" "$(VERSION)" "$(BUILD_NUM)" "$(MIN_MACOS)" | 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 | 62 | +# SwiftPM builds a fat binary in one command (arm64 + x86_64); products land in | ||
| 63 | −# aside before lipo. The -U flag allows the swiftCompatibility56 force-load | 63 | +# .build/apple/Products/Release. Verifies the slices with lipo. | ||
| 64 | −# symbol (CLT ships it arm64-only; never referenced at runtime on macOS 13+). | 64 | +PRODUCTS := .build/apple/Products/Release | ||
| 65 | universal: | 65 | universal: | ||
| 66 | @echo "=== Building universal binary (arm64 + x86_64) ===" | 66 | @echo "=== Building universal binary (arm64 + x86_64) ===" | ||
| 67 | − mkdir -p .build/universal | 67 | + swift build -c release --arch arm64 --arch x86_64 | ||
| 68 | − swift build -c release --arch arm64 | 68 | + lipo -archs "$(PRODUCTS)/$(EXEC_NAME)" | ||
| 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) | ||||
| 77 | 69 | ||||
| 78 | release: universal | 70 | release: universal | ||
| 79 | @echo "=== Assembling $(APP_DIR) (universal) ===" | 71 | @echo "=== Assembling $(APP_DIR) (universal) ===" | ||
| 80 | rm -rf "$(APP_DIR)" | 72 | rm -rf "$(APP_DIR)" | ||
| 81 | mkdir -p "$(APP_DIR)/Contents/MacOS" "$(APP_DIR)/Contents/Resources" | 73 | mkdir -p "$(APP_DIR)/Contents/MacOS" "$(APP_DIR)/Contents/Resources" | ||
| 82 | − cp .build/universal/$(EXEC_NAME) "$(APP_DIR)/Contents/MacOS/$(EXEC_NAME)" | 74 | + cp "$(PRODUCTS)/$(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 | 75 | + @for b in "$(PRODUCTS)"/*.bundle; do [ -e "$$b" ] && cp -R "$$b" "$(APP_DIR)/Contents/Resources/" || true; done | ||
| 84 | @if [ -f Resources/AppIcon.icns ]; then cp Resources/AppIcon.icns "$(APP_DIR)/Contents/Resources/AppIcon.icns"; fi | 76 | @if [ -f Resources/AppIcon.icns ]; then cp Resources/AppIcon.icns "$(APP_DIR)/Contents/Resources/AppIcon.icns"; fi | ||
| 85 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done | 77 | @for r in Resources/MenuBarIcon.png Resources/MenuBarIcon@2x.png; do [ -f "$$r" ] && cp "$$r" "$(APP_DIR)/Contents/Resources/" || true; done | ||
| 86 | scripts/write-info-plist.sh "$(APP_DIR)" "$(APP_NAME)" "$(EXEC_NAME)" "$(BUNDLE_ID)" "$(VERSION)" "$(BUILD_NUM)" "$(MIN_MACOS)" | 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 | ```sh | 18 | ```sh | ||
| 19 | make dev # release build → dist/Zyquo Atlas.app, ad-hoc signed (local iteration) | 19 | make dev # release build → dist/Zyquo Atlas.app, ad-hoc signed (local iteration) | ||
| 20 | make run # make dev, then launch the app | 20 | make run # make dev, then launch the app | ||
| 21 | −make test # run the test suite (Command Line Tools only) | 21 | +make test # run the test suite | ||
| 22 | −make release # universal, Developer ID signed, notarized, stapled (Phase 8) | 22 | +make release # universal (arm64+x86_64), Developer ID signed, notarized, stapled | ||
| 23 | −make verify # Phase 7 API verification with real keys (source .env.keys) | 23 | +make icon # regenerate AppIcon.icns from assets/icon/zyquo-atlas.svg | ||
| 24 | +make verify # API verification with real keys (source .env.keys) | ||||
| 24 | make clean | 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 | ### Toolchain note | 33 | ### Toolchain note | ||
| 28 | 34 | ||||
| 29 | The Command Line Tools' default SDK (macOS 27 beta) declares SwiftUI's `@State` | 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 | Author: Simon-Pierre Boucher | 7 | Author: Simon-Pierre Boucher | ||
| 8 | Mail: contact@spboucher.ai | 8 | Mail: contact@spboucher.ai | ||
| 9 | 9 | ||||
| 10 | − Phase 8 finalizes the Hardened Runtime posture. A WKWebView browser uses | 10 | + Signed with the Hardened Runtime (Developer ID distribution). Zyquo Atlas is | ||
| 11 | − WebKit XPC/child processes; the minimal set that lets WKWebView, downloads, | 11 | + a WKWebView browser: WebKit runs page JavaScript in Apple's own out-of-process | ||
| 12 | − and the app's own HTTPS API calls work is chosen and validated in Phase 8. | 12 | + WebContent/Networking/GPU XPC services (WebKit.framework is a *system* | ||
| 13 | − Not App-Sandboxed in v1 (Developer ID distribution, not Mac App Store). | 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 | <plist version="1.0"> | 20 | <plist version="1.0"> | ||
| 16 | <dict> | 21 | <dict> | ||
| 17 | <key>com.apple.security.cs.allow-jit</key> | 22 | <key>com.apple.security.cs.allow-jit</key> | ||
| 18 | − <true/> | 23 | + <false/> | ||
| 19 | − <key>com.apple.security.network.client</key> | ||||
| 20 | − <true/> | ||||
| 21 | </dict> | 24 | </dict> | ||
| 22 | </plist> | 25 | </plist> | ||
modified
docs/PLAN.md
+29 −0
@@ -165,3 +165,32 @@ launchable, single window; browser core is Phase 2). | |||||
| 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. | 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 | **Final: 183 checks · 183 passed · 0 failed.** No keys or browsing data committed. **Phase 7 gate PASSED.** | 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 | | Source | Page | Test | Result | Latency | Detail | | 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 | | — | `docs` | extract | ✅ | 1.7s | quality=reader, words=83, ~190tok, headings=0 | | 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 | | 23 | +| — | `js-app` | extract | ✅ | 1.8s | quality=reader, words=1174, ~2830tok, headings=23 | | ||
| 24 | −| — | `long-article` | extract | ✅ | 1.9s | quality=reader, words=28685, ~49595tok, headings=40 | | 24 | +| — | `long-article` | extract | ✅ | 1.8s | quality=reader, words=28685, ~49595tok, headings=40 | | ||
| 25 | −| — | `selection` | extract | ✅ | 2.4s | selection=In cartography, a map is a two-dimension, words=18106 | | 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 | |||||