diff --git a/.github/workflows/recover-v1.0.5-macos-arm64.yml b/.github/workflows/recover-v1.0.5-macos-arm64.yml
new file mode 100644
index 000000000..c83e10883
--- /dev/null
+++ b/.github/workflows/recover-v1.0.5-macos-arm64.yml
@@ -0,0 +1,901 @@
+name: Recover v1.0.5 macOS ARM64
+'on':
+ push:
+ branches:
+ - codex/recover-v1.0.5-macos-arm64
+permissions:
+ contents: read
+ actions: read
+concurrency:
+ group: recover-v1.0.5-macos-arm64
+ cancel-in-progress: false
+jobs:
+ build-web-ui:
+ name: web-dist
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: v1.0.5
+ - name: Check bundled models.dev freshness for release
+ if: 'true'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ python3 - <<'PY'
+ import datetime as dt
+ import json
+ from pathlib import Path
+
+ manifest_path = Path("assets/models_dev/MANIFEST.json")
+ try:
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ generated_at = dt.datetime.fromisoformat(
+ manifest["generated_at"].replace("Z", "+00:00")
+ )
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
+ raise SystemExit(f"Invalid {manifest_path}: {exc}")
+
+ if generated_at.tzinfo is None:
+ raise SystemExit(f"{manifest_path}: generated_at must include a timezone")
+ now = dt.datetime.now(dt.timezone.utc)
+ age = now - generated_at.astimezone(dt.timezone.utc)
+ if age < -dt.timedelta(minutes=5):
+ raise SystemExit(f"{manifest_path}: generated_at is in the future")
+ if age > dt.timedelta(days=30):
+ raise SystemExit(
+ f"Bundled models.dev registry is {age.days} days old; "
+ "run scripts/sync_models_dev before creating a release tag"
+ )
+ print(f"Bundled models.dev registry age: {age.days} days")
+ PY
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.32.1
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: pnpm
+ cache-dependency-path: web/pnpm-lock.yaml
+ - name: Build web UI
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ cd web
+ pnpm install --frozen-lockfile
+ pnpm build
+ - name: Upload web UI
+ uses: actions/upload-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ if-no-files-found: error
+ build-and-package:
+ name: ${{ matrix.id }}
+ needs: build-web-ui
+ runs-on: ${{ matrix.os }}
+ env:
+ VCPKG_BINARY_SOURCES: clear;default,readwrite
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - id: macos-arm64
+ os: macos-15
+ installer_arch: arm64
+ triplet: arm64-osx
+ executable: acecode
+ archive_extension: tar.gz
+ build_desktop: true
+ cmake_extra_args: -DCMAKE_OSX_ARCHITECTURES=arm64 -DACECODE_BUILD_DESKTOP=ON
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ ref: v1.0.5
+ - name: Verify immutable v1.0.5 source
+ shell: bash
+ run: |
+ test "$(git rev-parse HEAD)" = "f4dd8e767142cdd611d562f1bebf4634009925a8"
+ git diff --exit-code
+ - name: Resolve package version
+ id: package-version
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ declared_version="$(awk '$1 == "project(acecode" && $2 == "VERSION" { print $3; exit }' CMakeLists.txt)"
+ if [ -z "$declared_version" ]; then
+ echo "::error::Unable to read the ACECode version from CMakeLists.txt"
+ exit 1
+ fi
+ if [[ "$GITHUB_REF" == refs/tags/v* && "${GITHUB_REF_NAME#v}" != "$declared_version" ]]; then
+ echo "::error::Tag $GITHUB_REF_NAME does not match CMake version $declared_version"
+ exit 1
+ fi
+ echo "version=$declared_version" >> "$GITHUB_OUTPUT"
+ - name: Check macOS release credentials
+ if: runner.os == 'macOS'
+ id: macos-release
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ application_missing=()
+ for name in \
+ MACOS_CERTIFICATE_BASE64 \
+ MACOS_CERTIFICATE_PASSWORD \
+ APPLE_ID \
+ APPLE_TEAM_ID \
+ APPLE_APP_SPECIFIC_PASSWORD; do
+ if [ -z "${!name}" ]; then
+ application_missing+=("$name")
+ fi
+ done
+
+ if [ "${#application_missing[@]}" -ne 0 ]; then
+ if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS application releases require secrets: ${application_missing[*]}"
+ exit 1
+ fi
+ echo "::notice::macOS signing/notarization disabled; missing: ${application_missing[*]}"
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ installer_certificate_set=false
+ installer_password_set=false
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_BASE64" ]; then
+ installer_certificate_set=true
+ fi
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" ]; then
+ installer_password_set=true
+ fi
+
+ if [ "$installer_certificate_set" != "$installer_password_set" ]; then
+ echo "::error::macOS Installer credentials must configure both MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD"
+ exit 1
+ fi
+ if [ "$installer_certificate_set" = "true" ]; then
+ if [ "${#application_missing[@]}" -eq 0 ]; then
+ echo "pkg_enabled=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "::notice::macOS PKG creation disabled because application signing/notarization is disabled"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS releases require MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD to build both PKGs"
+ exit 1
+ fi
+ echo "::notice::macOS PKG creation disabled; Developer ID Installer credentials are not configured"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Configure MSVC
+ if: runner.os == 'Windows'
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ matrix.msvc_arch }}
+ - name: Install Linux ARMv7 cross toolchain
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ sudo apt-get update
+ sudo apt-get install -y \
+ binutils-arm-linux-gnueabihf \
+ gcc-arm-linux-gnueabihf \
+ g++-arm-linux-gnueabihf \
+ qemu-user
+ - name: Run vcpkg
+ uses: lukka/run-vcpkg@v11
+ - name: Restore vcpkg binary cache
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
+ key: vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**')
+ }}
+ restore-keys: |
+ vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-
+ - name: Install Linux desktop dependencies
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev
+ - name: Download web UI
+ uses: actions/download-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ - name: Configure
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=MinSizeRel \
+ -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
+ -DVCPKG_TARGET_TRIPLET="${{ matrix.triplet }}" \
+ -DBUILD_TESTING=OFF \
+ ${{ matrix.cmake_extra_args }}
+ - name: Build
+ shell: bash
+ run: |-
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ cmake --build build --config MinSizeRel --target acecode
+ - name: Build desktop
+ if: matrix.build_desktop
+ shell: bash
+ run: |-
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ cmake --build build --config MinSizeRel --target acecode-desktop
+ - name: Verify Linux ARMv7 executable
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ export LC_ALL=C
+ elf_header="$(arm-linux-gnueabihf-readelf -h build/acecode)"
+ arm_attributes="$(arm-linux-gnueabihf-readelf -A build/acecode)"
+ program_headers="$(arm-linux-gnueabihf-readelf -l build/acecode)"
+
+ printf '%s\n' "$elf_header"
+ printf '%s\n' "$arm_attributes"
+
+ grep -Eq 'Class:[[:space:]]+ELF32' <<<"$elf_header"
+ grep -Eq 'Machine:[[:space:]]+ARM' <<<"$elf_header"
+ grep -Eq 'Tag_CPU_arch:[[:space:]]+v7' <<<"$arm_attributes"
+ grep -Eq 'Tag_ABI_VFP_args:[[:space:]]+VFP registers' <<<"$arm_attributes"
+ grep -Fq '/lib/ld-linux-armhf.so.3' <<<"$program_headers"
+
+ version_output="$(qemu-arm -L /usr/arm-linux-gnueabihf build/acecode --version)"
+ printf '%s\n' "$version_output"
+ grep -Eq '^acecode v[^[:space:]]+$' <<<"$version_output"
+ - name: Verify bundled resources in macOS app
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ registry_dir="build/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev resources in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched macOS app models.dev resource: $file" >&2
+ exit 1
+ fi
+ done
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged build/ACECode.app/Contents/Resources/share/acecode/seed
+ - name: Extract debug symbols (Linux)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ binutils_prefix=""
+ if [ "${{ matrix.id }}" = "linux-armv7" ]; then
+ binutils_prefix="arm-linux-gnueabihf-"
+ fi
+ "${binutils_prefix}objcopy" --only-keep-debug \
+ "build/${{ matrix.executable }}" \
+ "build/${{ matrix.executable }}.debug"
+ "${binutils_prefix}strip" --strip-unneeded "build/${{ matrix.executable }}"
+ "${binutils_prefix}objcopy" \
+ --add-gnu-debuglink="build/${{ matrix.executable }}.debug" \
+ "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ objcopy --only-keep-debug "build/acecode-desktop" "build/acecode-desktop.debug"
+ strip --strip-unneeded "build/acecode-desktop"
+ objcopy --add-gnu-debuglink="build/acecode-desktop.debug" "build/acecode-desktop"
+ - name: Extract debug symbols (macOS)
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ dsymutil "build/${{ matrix.executable }}" -o "build/${{ matrix.executable }}.dSYM"
+ strip "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ app_exec="build/ACECode.app/Contents/MacOS/ACECode"
+ app_daemon="build/ACECode.app/Contents/MacOS/acecode-daemon"
+ if [ ! -f "$app_exec" ]; then
+ echo "Missing desktop app executable: $app_exec" >&2
+ exit 1
+ fi
+ if [ ! -f "$app_daemon" ]; then
+ echo "Missing desktop app daemon: $app_daemon" >&2
+ exit 1
+ fi
+ dsymutil "$app_exec" -o "build/ACECode.app.dSYM"
+ strip "$app_exec"
+ dsymutil "$app_daemon" -o "build/acecode-daemon.dSYM"
+ strip "$app_daemon"
+ - name: Import Developer ID identities (macOS)
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-keychain
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ MACOS_CODESIGN_IDENTITY: ${{ vars.MACOS_CODESIGN_IDENTITY }}
+ MACOS_INSTALLER_SIGNING_IDENTITY: ${{ vars.MACOS_INSTALLER_SIGNING_IDENTITY }}
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ set +x
+ certificate_path="$RUNNER_TEMP/acecode-developer-id.p12"
+ installer_certificate_path="$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ keychain_path="$RUNNER_TEMP/acecode-signing.keychain-db"
+ keychain_password="$(openssl rand -hex 32)"
+
+ printf '%s' "$MACOS_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$certificate_path"
+ chmod 600 "$certificate_path"
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ printf '%s' "$MACOS_INSTALLER_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$installer_certificate_path"
+ chmod 600 "$installer_certificate_path"
+ fi
+ security create-keychain -p "$keychain_password" "$keychain_path"
+ security set-keychain-settings -lut 21600 "$keychain_path"
+ security unlock-keychain -p "$keychain_password" "$keychain_path"
+
+ existing_user_keychains=()
+ while IFS= read -r existing_keychain; do
+ if [ -n "$existing_keychain" ]; then
+ existing_user_keychains+=("$existing_keychain")
+ fi
+ done < <(
+ security list-keychains -d user |
+ sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//'
+ )
+ security list-keychains -d user -s \
+ "$keychain_path" \
+ "${existing_user_keychains[@]}"
+
+ security import "$certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/codesign \
+ -T /usr/bin/security
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ security import "$installer_certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/pkgbuild \
+ -T /usr/bin/productbuild \
+ -T /usr/bin/security
+ fi
+ security set-key-partition-list \
+ -S apple-tool:,apple: \
+ -s \
+ -k "$keychain_password" \
+ "$keychain_path"
+
+ identity_record="$(security find-identity -v -p codesigning "$keychain_path" |
+ awk '/Developer ID Application:/ { print; exit }')"
+ identity_fingerprint="$(printf '%s\n' "$identity_record" | awk '{ print $2 }')"
+ identity_name="$(printf '%s\n' "$identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$identity_name" ]; then
+ echo "::error::No Developer ID Application identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_CODESIGN_IDENTITY" ] && [ "$identity_name" != "$MACOS_CODESIGN_IDENTITY" ]; then
+ echo "::error::Imported signing identity does not match MACOS_CODESIGN_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Application identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ installer_identity_record="$(security find-identity -v -p basic "$keychain_path" |
+ awk '/Developer ID Installer:/ { print; exit }')"
+ installer_identity_fingerprint="$(printf '%s\n' "$installer_identity_record" | awk '{ print $2 }')"
+ installer_identity_name="$(printf '%s\n' "$installer_identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$installer_identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$installer_identity_name" ]; then
+ echo "::error::No Developer ID Installer identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_INSTALLER_SIGNING_IDENTITY" ] && [ "$installer_identity_name" != "$MACOS_INSTALLER_SIGNING_IDENTITY" ]; then
+ echo "::error::Imported Installer identity does not match MACOS_INSTALLER_SIGNING_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$installer_identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Installer identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+ echo "installer_identity=$installer_identity_name" >> "$GITHUB_OUTPUT"
+ fi
+
+ echo "keychain=$keychain_path" >> "$GITHUB_OUTPUT"
+ # Sign by the certificate fingerprint. codesign can fail to resolve a
+ # human-readable identity name inside an isolated temporary keychain.
+ echo "identity=$identity_fingerprint" >> "$GITHUB_OUTPUT"
+ - name: Sign macOS release payloads
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ scripts/macos_codesign.sh \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}" \
+ --binary "build/${{ matrix.executable }}" \
+ --binary "build/acecode-computer-use" \
+ --app "build/ACECode.app"
+ - name: Notarize and staple macOS app
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ scripts/macos_notarize_app.sh --app "build/ACECode.app"
+ - name: Test macOS installer integration
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ bash tests/scripts/macos_installer_test.sh
+ python3 tests/scripts/macos_installer_release_test.py
+ - name: Create current-user macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ id: macos-pkg
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ pkg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.pkg"
+ scripts/macos_create_pkg.sh \
+ --app "build/ACECode.app" \
+ --output "$pkg_path" \
+ --installer-identity "${{ steps.macos-keychain.outputs.installer_identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$pkg_path" >> "$GITHUB_OUTPUT"
+ - name: Notarize and validate macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ scripts/macos_notarize_pkg.sh --pkg "${{ steps.macos-pkg.outputs.path }}"
+ - name: Upload macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pkg
+ path: ${{ steps.macos-pkg.outputs.path }}
+ if-no-files-found: error
+ - name: Build signed and notarized macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-dmg
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ dmg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.dmg"
+ bash scripts/macos_create_installer_dmg.sh \
+ --app "build/ACECode.app" \
+ --output "$dmg_path" \
+ --arch "${{ matrix.installer_arch }}" \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$dmg_path" >> "$GITHUB_OUTPUT"
+ - name: Create macOS self-update archive
+ if: runner.os == 'macOS'
+ id: macos-update
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ unsigned_suffix=""
+ trust_args=()
+ if [ "${{ steps.macos-release.outputs.enabled }}" = "true" ]; then
+ trust_args+=(--require-trusted)
+ else
+ unsigned_suffix="-unsigned"
+ fi
+ update_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update${unsigned_suffix}.zip"
+ scripts/macos_create_update_zip.sh \
+ --app "build/ACECode.app" \
+ --output "$update_path" \
+ "${trust_args[@]}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Package (Unix)
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ package_dir="dist/acecode-${{ matrix.id }}"
+ mkdir -p "$package_dir"
+
+ validate_models_dev_registry() {
+ registry_dir="$1"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev files in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched models.dev package file: $registry_dir/$file" >&2
+ exit 1
+ fi
+ done
+ }
+
+ if [ ! -f "build/${{ matrix.executable }}" ]; then
+ echo "Missing terminal executable: build/${{ matrix.executable }}" >&2
+ exit 1
+ fi
+ cp "build/${{ matrix.executable }}" "$package_dir/"
+ cp README.md README_CN.md "$package_dir/"
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component models_dev_registry
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component default_seed_bundle
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component whatsapp_channel
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component computer_use_runtime
+ test -x "$package_dir/acecode-computer-use"
+ test -x "build/ACECode.app/Contents/MacOS/acecode-computer-use"
+ fi
+ validate_models_dev_registry "$package_dir/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$package_dir/share/acecode/seed"
+
+ if [ "${{ matrix.build_desktop }}" = "true" ]; then
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ if [ ! -d "build/ACECode.app" ]; then
+ echo "Missing desktop app bundle: build/ACECode.app" >&2
+ exit 1
+ fi
+ cp -r "build/ACECode.app" "$package_dir/"
+ else
+ if [ ! -f "build/acecode-desktop" ]; then
+ echo "Missing desktop executable: build/acecode-desktop" >&2
+ exit 1
+ fi
+ cp "build/acecode-desktop" "$package_dir/"
+ cp "build/acecode-logo.png" "$package_dir/"
+ fi
+ fi
+ legacy="$(find "$package_dir" -maxdepth 1 -iname 'ace-browser-*' -print -quit)"
+ if [ -n "$legacy" ]; then
+ echo "Legacy browser artifact must not be packaged: $legacy" >&2
+ exit 1
+ fi
+ tar -C dist -czf "acecode-${{ matrix.id }}.tar.gz" "acecode-${{ matrix.id }}"
+
+ verify_root="$(mktemp -d)"
+ tar -xzf "acecode-${{ matrix.id }}.tar.gz" -C "$verify_root"
+ extracted_package="$verify_root/acecode-${{ matrix.id }}"
+ validate_models_dev_registry "$extracted_package/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/share/acecode/seed"
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ test -x "$extracted_package/acecode-computer-use"
+ test -x "$extracted_package/ACECode.app/Contents/MacOS/acecode-computer-use"
+ validate_models_dev_registry \
+ "$extracted_package/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/ACECode.app/Contents/Resources/share/acecode/seed"
+ fi
+ rm -rf -- "$verify_root"
+ - name: Create Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ id: linux-update
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ update_path="acecode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update.zip"
+ bash scripts/linux_create_update_zip.sh \
+ --package-dir "dist/acecode-${{ matrix.id }}" \
+ --output "$update_path" \
+ --expected-version "${{ steps.package-version.outputs.version }}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Clean up macOS signing material
+ if: always() && runner.os == 'macOS'
+ shell: bash
+ run: |
+ GITHUB_REF=refs/tags/v1.0.5
+ GITHUB_REF_NAME=v1.0.5
+ set +e
+ security delete-keychain "$RUNNER_TEMP/acecode-signing.keychain-db" 2>/dev/null
+ rm -f -- \
+ "$RUNNER_TEMP/acecode-developer-id.p12" \
+ "$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ - name: Package (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
+
+ function Assert-ModelsDevRegistry([string] $RegistryDir) {
+ if (-not (Test-Path -LiteralPath $RegistryDir -PathType Container)) {
+ throw "Missing models.dev directory: $RegistryDir"
+ }
+ $actualFiles = @(Get-ChildItem -LiteralPath $RegistryDir -File -Recurse)
+ if ($actualFiles.Count -ne 3) {
+ throw "Expected exactly 3 models.dev files in ${RegistryDir}, found $($actualFiles.Count)"
+ }
+ foreach ($name in @("api.json", "MANIFEST.json", "LICENSE")) {
+ $source = Join-Path "assets/models_dev" $name
+ $packaged = Join-Path $RegistryDir $name
+ if (-not (Test-Path -LiteralPath $packaged -PathType Leaf)) {
+ throw "Missing models.dev package file: $packaged"
+ }
+ $sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
+ $packagedHash = (Get-FileHash -LiteralPath $packaged -Algorithm SHA256).Hash
+ if ($sourceHash -ne $packagedHash) {
+ throw "Mismatched models.dev package file: $packaged"
+ }
+ }
+ }
+
+ $terminalExe = "build/${{ matrix.executable }}"
+ if (-not (Test-Path -LiteralPath $terminalExe)) {
+ throw "Missing terminal executable: $terminalExe"
+ }
+ Copy-Item -LiteralPath $terminalExe -Destination "$packageDir/"
+ Copy-Item "README.md","README_CN.md" "$packageDir/"
+ $packageRoot = (Resolve-Path -LiteralPath $packageDir).Path
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component models_dev_registry
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the bundled models.dev registry"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component default_seed_bundle
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the default seed bundle"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component whatsapp_channel
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the WhatsApp channel bridge"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component computer_use_runtime
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the Computer Use runtime"
+ }
+ $computerUseExe = Join-Path $packageRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $computerUseExe -PathType Leaf) -or
+ (Get-Item -LiteralPath $computerUseExe).Length -eq 0) {
+ throw "Missing Computer Use runtime: $computerUseExe"
+ }
+ Assert-ModelsDevRegistry (Join-Path $packageDir "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $packageDir "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the packaged default seed bundle"
+ }
+
+ if ("${{ matrix.build_desktop }}" -eq "true") {
+ $desktopCandidates = @(
+ "build/acecode-desktop.exe",
+ "build/Release/acecode-desktop.exe",
+ "build/Debug/acecode-desktop.exe",
+ "build/RelWithDebInfo/acecode-desktop.exe",
+ "build/MinSizeRel/acecode-desktop.exe"
+ )
+ $desktopExe = $desktopCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
+ if (-not $desktopExe) {
+ $desktopExe = Get-ChildItem -Path "build" -Recurse -Filter "acecode-desktop.exe" -File |
+ Select-Object -ExpandProperty FullName -First 1
+ }
+ if (-not $desktopExe) {
+ throw "Missing desktop executable: acecode-desktop.exe"
+ }
+ Copy-Item -LiteralPath $desktopExe -Destination "$packageDir/"
+ }
+ $legacyBrowserArtifact = Get-ChildItem -LiteralPath $packageDir -Filter "ace-browser-*" -Force |
+ Select-Object -First 1
+ if ($legacyBrowserArtifact) {
+ throw "Legacy browser artifact must not be packaged: $($legacyBrowserArtifact.FullName)"
+ }
+ Compress-Archive -Path "$packageDir/*" -DestinationPath "acecode-${{ matrix.id }}.zip" -Force
+
+ $verifyRoot = Join-Path $env:RUNNER_TEMP ("acecode-package-verify-" + [Guid]::NewGuid().ToString("N"))
+ New-Item -ItemType Directory -Path $verifyRoot | Out-Null
+ Expand-Archive -LiteralPath "acecode-${{ matrix.id }}.zip" -DestinationPath $verifyRoot
+ $archivedComputerUseExe = Join-Path $verifyRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $archivedComputerUseExe -PathType Leaf) -or
+ (Get-FileHash -LiteralPath $archivedComputerUseExe -Algorithm SHA256).Hash -ne
+ (Get-FileHash -LiteralPath $computerUseExe -Algorithm SHA256).Hash) {
+ throw "Archived Computer Use runtime is missing or does not match the build"
+ }
+ Assert-ModelsDevRegistry (Join-Path $verifyRoot "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $verifyRoot "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the archived default seed bundle"
+ }
+ - name: Verify Windows ARM64 package
+ if: matrix.id == 'windows-arm64'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ $expectedExecutables = @(
+ "acecode.exe",
+ "acecode-desktop.exe",
+ "acecode-computer-use.exe"
+ )
+ foreach ($name in $expectedExecutables) {
+ $path = Join-Path $packageDir $name
+ if (-not (Test-Path -LiteralPath $path)) {
+ throw "Missing ARM64 package executable: $path"
+ }
+ $headers = (& dumpbin /headers $path | Out-String)
+ if ($LASTEXITCODE -ne 0 -or $headers -notmatch '(?im)\bAA64 machine\b') {
+ throw "$name is not an ARM64 PE executable"
+ }
+ Write-Host "${name}: ARM64"
+ }
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}
+ path: acecode-${{ matrix.id }}.${{ matrix.archive_extension }}
+ if-no-files-found: error
+ - name: Upload Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.linux-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dmg
+ path: ${{ steps.macos-dmg.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS self-update archive
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.macos-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload PDB
+ if: runner.os == 'Windows'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pdb
+ path: build/**/acecode.pdb
+ if-no-files-found: warn
+ - name: Upload desktop PDB
+ if: runner.os == 'Windows' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-pdb
+ path: build/**/acecode-desktop.pdb
+ if-no-files-found: warn
+ - name: Upload debug symbols (Linux)
+ if: runner.os == 'Linux'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-debug
+ path: build/${{ matrix.executable }}.debug
+ if-no-files-found: warn
+ - name: Upload desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-debug
+ path: build/acecode-desktop.debug
+ if-no-files-found: warn
+ - name: Upload dSYM (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dsym
+ path: build/${{ matrix.executable }}.dSYM
+ if-no-files-found: warn
+ - name: Upload desktop dSYM (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-dsym
+ path: |
+ build/ACECode.app.dSYM
+ build/acecode-daemon.dSYM
+ if-no-files-found: warn
diff --git a/.github/workflows/recover-v1.0.6-platform-assets.yml b/.github/workflows/recover-v1.0.6-platform-assets.yml
new file mode 100644
index 000000000..e2973bf8b
--- /dev/null
+++ b/.github/workflows/recover-v1.0.6-platform-assets.yml
@@ -0,0 +1,876 @@
+name: Recover v1.0.6 Windows x64 asset
+'on':
+ push:
+ branches:
+ - codex/recover-v1.0.6-platform-assets
+permissions:
+ contents: read
+ actions: read
+concurrency:
+ group: recover-v1.0.6-windows-x64
+ cancel-in-progress: false
+jobs:
+ build-web-ui:
+ name: web-dist
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: v1.0.6
+ - name: Verify immutable v1.0.6 source
+ shell: bash
+ run: |
+ test "$(git rev-parse HEAD)" = "e090be90c668f5fce760ed9b04d3f9a677d030eb"
+ git diff --exit-code
+ - name: Check bundled models.dev freshness for release
+ if: 'true'
+ shell: bash
+ run: |
+ python3 - <<'PY'
+ import datetime as dt
+ import json
+ from pathlib import Path
+
+ manifest_path = Path("assets/models_dev/MANIFEST.json")
+ try:
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ generated_at = dt.datetime.fromisoformat(
+ manifest["generated_at"].replace("Z", "+00:00")
+ )
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
+ raise SystemExit(f"Invalid {manifest_path}: {exc}")
+
+ if generated_at.tzinfo is None:
+ raise SystemExit(f"{manifest_path}: generated_at must include a timezone")
+ now = dt.datetime.now(dt.timezone.utc)
+ age = now - generated_at.astimezone(dt.timezone.utc)
+ if age < -dt.timedelta(minutes=5):
+ raise SystemExit(f"{manifest_path}: generated_at is in the future")
+ if age > dt.timedelta(days=30):
+ raise SystemExit(
+ f"Bundled models.dev registry is {age.days} days old; "
+ "run scripts/sync_models_dev before creating a release tag"
+ )
+ print(f"Bundled models.dev registry age: {age.days} days")
+ PY
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.32.1
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: pnpm
+ cache-dependency-path: web/pnpm-lock.yaml
+ - name: Build web UI
+ shell: bash
+ run: |
+ cd web
+ pnpm install --frozen-lockfile
+ pnpm build
+ - name: Upload web UI
+ uses: actions/upload-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ if-no-files-found: error
+ timeout-minutes: 60
+ env:
+ ACE_RECOVERY_REF: refs/tags/v1.0.6
+ ACE_RECOVERY_REF_NAME: v1.0.6
+ build-and-package:
+ name: ${{ matrix.id }}
+ needs: build-web-ui
+ runs-on: ${{ matrix.os }}
+ env:
+ VCPKG_BINARY_SOURCES: clear;default,readwrite
+ ACE_RECOVERY_REF: refs/tags/v1.0.6
+ ACE_RECOVERY_REF_NAME: v1.0.6
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - id: windows-x64
+ os: windows-2022
+ triplet: x64-windows-static
+ executable: acecode.exe
+ archive_extension: zip
+ msvc_arch: amd64
+ build_desktop: true
+ cmake_extra_args: -DACECODE_BUILD_DESKTOP=ON
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ ref: v1.0.6
+ - name: Verify immutable v1.0.6 source
+ shell: bash
+ run: |
+ test "$(git rev-parse HEAD)" = "e090be90c668f5fce760ed9b04d3f9a677d030eb"
+ git diff --exit-code
+ - name: Resolve package version
+ id: package-version
+ shell: bash
+ run: |
+ declared_version="$(awk '$1 == "project(acecode" && $2 == "VERSION" { print $3; exit }' CMakeLists.txt)"
+ if [ -z "$declared_version" ]; then
+ echo "::error::Unable to read the ACECode version from CMakeLists.txt"
+ exit 1
+ fi
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* && "${ACE_RECOVERY_REF_NAME#v}" != "$declared_version" ]]; then
+ echo "::error::Tag $ACE_RECOVERY_REF_NAME does not match CMake version $declared_version"
+ exit 1
+ fi
+ echo "version=$declared_version" >> "$GITHUB_OUTPUT"
+ - name: Check macOS release credentials
+ if: runner.os == 'macOS'
+ id: macos-release
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ application_missing=()
+ for name in \
+ MACOS_CERTIFICATE_BASE64 \
+ MACOS_CERTIFICATE_PASSWORD \
+ APPLE_ID \
+ APPLE_TEAM_ID \
+ APPLE_APP_SPECIFIC_PASSWORD; do
+ if [ -z "${!name}" ]; then
+ application_missing+=("$name")
+ fi
+ done
+
+ if [ "${#application_missing[@]}" -ne 0 ]; then
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS application releases require secrets: ${application_missing[*]}"
+ exit 1
+ fi
+ echo "::notice::macOS signing/notarization disabled; missing: ${application_missing[*]}"
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ installer_certificate_set=false
+ installer_password_set=false
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_BASE64" ]; then
+ installer_certificate_set=true
+ fi
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" ]; then
+ installer_password_set=true
+ fi
+
+ if [ "$installer_certificate_set" != "$installer_password_set" ]; then
+ echo "::error::macOS Installer credentials must configure both MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD"
+ exit 1
+ fi
+ if [ "$installer_certificate_set" = "true" ]; then
+ if [ "${#application_missing[@]}" -eq 0 ]; then
+ echo "pkg_enabled=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "::notice::macOS PKG creation disabled because application signing/notarization is disabled"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS releases require MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD to build both PKGs"
+ exit 1
+ fi
+ echo "::notice::macOS PKG creation disabled; Developer ID Installer credentials are not configured"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Configure MSVC
+ if: runner.os == 'Windows'
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ matrix.msvc_arch }}
+ - name: Install Linux ARMv7 cross toolchain
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ binutils-arm-linux-gnueabihf \
+ gcc-arm-linux-gnueabihf \
+ g++-arm-linux-gnueabihf \
+ qemu-user
+ - name: Run vcpkg
+ uses: lukka/run-vcpkg@v11
+ - name: Restore vcpkg binary cache
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
+ key: vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**')
+ }}
+ restore-keys: |
+ vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-
+ - name: Install Linux desktop dependencies
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev
+ - name: Download web UI
+ uses: actions/download-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ - name: Configure
+ shell: bash
+ run: |
+ cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=MinSizeRel \
+ -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
+ -DVCPKG_TARGET_TRIPLET="${{ matrix.triplet }}" \
+ -DBUILD_TESTING=OFF \
+ ${{ matrix.cmake_extra_args }}
+ - name: Build
+ shell: bash
+ run: |
+ cmake --build build --config MinSizeRel --target acecode --parallel 2
+ - name: Build desktop
+ if: matrix.build_desktop
+ shell: bash
+ run: |
+ cmake --build build --config MinSizeRel --target acecode-desktop --parallel 2
+ - name: Verify Linux ARMv7 executable
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ export LC_ALL=C
+ elf_header="$(arm-linux-gnueabihf-readelf -h build/acecode)"
+ arm_attributes="$(arm-linux-gnueabihf-readelf -A build/acecode)"
+ program_headers="$(arm-linux-gnueabihf-readelf -l build/acecode)"
+
+ printf '%s\n' "$elf_header"
+ printf '%s\n' "$arm_attributes"
+
+ grep -Eq 'Class:[[:space:]]+ELF32' <<<"$elf_header"
+ grep -Eq 'Machine:[[:space:]]+ARM' <<<"$elf_header"
+ grep -Eq 'Tag_CPU_arch:[[:space:]]+v7' <<<"$arm_attributes"
+ grep -Eq 'Tag_ABI_VFP_args:[[:space:]]+VFP registers' <<<"$arm_attributes"
+ grep -Fq '/lib/ld-linux-armhf.so.3' <<<"$program_headers"
+
+ version_output="$(qemu-arm -L /usr/arm-linux-gnueabihf build/acecode --version)"
+ printf '%s\n' "$version_output"
+ grep -Eq '^acecode v[^[:space:]]+$' <<<"$version_output"
+ - name: Verify bundled resources in macOS app
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ registry_dir="build/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev resources in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched macOS app models.dev resource: $file" >&2
+ exit 1
+ fi
+ done
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged build/ACECode.app/Contents/Resources/share/acecode/seed
+ - name: Extract debug symbols (Linux)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ binutils_prefix=""
+ if [ "${{ matrix.id }}" = "linux-armv7" ]; then
+ binutils_prefix="arm-linux-gnueabihf-"
+ fi
+ "${binutils_prefix}objcopy" --only-keep-debug \
+ "build/${{ matrix.executable }}" \
+ "build/${{ matrix.executable }}.debug"
+ "${binutils_prefix}strip" --strip-unneeded "build/${{ matrix.executable }}"
+ "${binutils_prefix}objcopy" \
+ --add-gnu-debuglink="build/${{ matrix.executable }}.debug" \
+ "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ objcopy --only-keep-debug "build/acecode-desktop" "build/acecode-desktop.debug"
+ strip --strip-unneeded "build/acecode-desktop"
+ objcopy --add-gnu-debuglink="build/acecode-desktop.debug" "build/acecode-desktop"
+ - name: Extract debug symbols (macOS)
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ dsymutil "build/${{ matrix.executable }}" -o "build/${{ matrix.executable }}.dSYM"
+ strip "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ app_exec="build/ACECode.app/Contents/MacOS/ACECode"
+ app_daemon="build/ACECode.app/Contents/MacOS/acecode-daemon"
+ if [ ! -f "$app_exec" ]; then
+ echo "Missing desktop app executable: $app_exec" >&2
+ exit 1
+ fi
+ if [ ! -f "$app_daemon" ]; then
+ echo "Missing desktop app daemon: $app_daemon" >&2
+ exit 1
+ fi
+ dsymutil "$app_exec" -o "build/ACECode.app.dSYM"
+ strip "$app_exec"
+ dsymutil "$app_daemon" -o "build/acecode-daemon.dSYM"
+ strip "$app_daemon"
+ - name: Import Developer ID identities (macOS)
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-keychain
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ MACOS_CODESIGN_IDENTITY: ${{ vars.MACOS_CODESIGN_IDENTITY }}
+ MACOS_INSTALLER_SIGNING_IDENTITY: ${{ vars.MACOS_INSTALLER_SIGNING_IDENTITY }}
+ run: |
+ set +x
+ certificate_path="$RUNNER_TEMP/acecode-developer-id.p12"
+ installer_certificate_path="$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ keychain_path="$RUNNER_TEMP/acecode-signing.keychain-db"
+ keychain_password="$(openssl rand -hex 32)"
+
+ printf '%s' "$MACOS_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$certificate_path"
+ chmod 600 "$certificate_path"
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ printf '%s' "$MACOS_INSTALLER_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$installer_certificate_path"
+ chmod 600 "$installer_certificate_path"
+ fi
+ security create-keychain -p "$keychain_password" "$keychain_path"
+ security set-keychain-settings -lut 21600 "$keychain_path"
+ security unlock-keychain -p "$keychain_password" "$keychain_path"
+
+ existing_user_keychains=()
+ while IFS= read -r existing_keychain; do
+ if [ -n "$existing_keychain" ]; then
+ existing_user_keychains+=("$existing_keychain")
+ fi
+ done < <(
+ security list-keychains -d user |
+ sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//'
+ )
+ security list-keychains -d user -s \
+ "$keychain_path" \
+ "${existing_user_keychains[@]}"
+
+ security import "$certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/codesign \
+ -T /usr/bin/security
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ security import "$installer_certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/pkgbuild \
+ -T /usr/bin/productbuild \
+ -T /usr/bin/security
+ fi
+ security set-key-partition-list \
+ -S apple-tool:,apple: \
+ -s \
+ -k "$keychain_password" \
+ "$keychain_path"
+
+ identity_record="$(security find-identity -v -p codesigning "$keychain_path" |
+ awk '/Developer ID Application:/ { print; exit }')"
+ identity_fingerprint="$(printf '%s\n' "$identity_record" | awk '{ print $2 }')"
+ identity_name="$(printf '%s\n' "$identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$identity_name" ]; then
+ echo "::error::No Developer ID Application identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_CODESIGN_IDENTITY" ] && [ "$identity_name" != "$MACOS_CODESIGN_IDENTITY" ]; then
+ echo "::error::Imported signing identity does not match MACOS_CODESIGN_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Application identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ installer_identity_record="$(security find-identity -v -p basic "$keychain_path" |
+ awk '/Developer ID Installer:/ { print; exit }')"
+ installer_identity_fingerprint="$(printf '%s\n' "$installer_identity_record" | awk '{ print $2 }')"
+ installer_identity_name="$(printf '%s\n' "$installer_identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$installer_identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$installer_identity_name" ]; then
+ echo "::error::No Developer ID Installer identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_INSTALLER_SIGNING_IDENTITY" ] && [ "$installer_identity_name" != "$MACOS_INSTALLER_SIGNING_IDENTITY" ]; then
+ echo "::error::Imported Installer identity does not match MACOS_INSTALLER_SIGNING_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$installer_identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Installer identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+ echo "installer_identity=$installer_identity_name" >> "$GITHUB_OUTPUT"
+ fi
+
+ echo "keychain=$keychain_path" >> "$GITHUB_OUTPUT"
+ # Sign by the certificate fingerprint. codesign can fail to resolve a
+ # human-readable identity name inside an isolated temporary keychain.
+ echo "identity=$identity_fingerprint" >> "$GITHUB_OUTPUT"
+ - name: Sign macOS release payloads
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ run: |
+ scripts/macos_codesign.sh \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}" \
+ --binary "build/${{ matrix.executable }}" \
+ --binary "build/acecode-computer-use" \
+ --app "build/ACECode.app"
+ - name: Notarize and staple macOS app
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ scripts/macos_notarize_app.sh --app "build/ACECode.app"
+ - name: Test macOS installer integration
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ bash tests/scripts/macos_installer_test.sh
+ python3 tests/scripts/macos_installer_release_test.py
+ - name: Create current-user macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ id: macos-pkg
+ shell: bash
+ run: |
+ pkg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.pkg"
+ scripts/macos_create_pkg.sh \
+ --app "build/ACECode.app" \
+ --output "$pkg_path" \
+ --installer-identity "${{ steps.macos-keychain.outputs.installer_identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$pkg_path" >> "$GITHUB_OUTPUT"
+ - name: Notarize and validate macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ scripts/macos_notarize_pkg.sh --pkg "${{ steps.macos-pkg.outputs.path }}"
+ - name: Upload macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pkg
+ path: ${{ steps.macos-pkg.outputs.path }}
+ if-no-files-found: error
+ - name: Build signed and notarized macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-dmg
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ dmg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.dmg"
+ bash scripts/macos_create_installer_dmg.sh \
+ --app "build/ACECode.app" \
+ --output "$dmg_path" \
+ --arch "${{ matrix.installer_arch }}" \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$dmg_path" >> "$GITHUB_OUTPUT"
+ - name: Create macOS self-update archive
+ if: runner.os == 'macOS'
+ id: macos-update
+ shell: bash
+ run: |
+ unsigned_suffix=""
+ trust_args=()
+ if [ "${{ steps.macos-release.outputs.enabled }}" = "true" ]; then
+ trust_args+=(--require-trusted)
+ else
+ unsigned_suffix="-unsigned"
+ fi
+ update_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update${unsigned_suffix}.zip"
+ scripts/macos_create_update_zip.sh \
+ --app "build/ACECode.app" \
+ --output "$update_path" \
+ "${trust_args[@]}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Package (Unix)
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ package_dir="dist/acecode-${{ matrix.id }}"
+ mkdir -p "$package_dir"
+
+ validate_models_dev_registry() {
+ registry_dir="$1"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev files in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched models.dev package file: $registry_dir/$file" >&2
+ exit 1
+ fi
+ done
+ }
+
+ if [ ! -f "build/${{ matrix.executable }}" ]; then
+ echo "Missing terminal executable: build/${{ matrix.executable }}" >&2
+ exit 1
+ fi
+ cp "build/${{ matrix.executable }}" "$package_dir/"
+ cp README.md README_CN.md "$package_dir/"
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component models_dev_registry
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component default_seed_bundle
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component whatsapp_channel
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component computer_use_runtime
+ test -x "$package_dir/acecode-computer-use"
+ test -x "build/ACECode.app/Contents/MacOS/acecode-computer-use"
+ fi
+ validate_models_dev_registry "$package_dir/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$package_dir/share/acecode/seed"
+
+ if [ "${{ matrix.build_desktop }}" = "true" ]; then
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ if [ ! -d "build/ACECode.app" ]; then
+ echo "Missing desktop app bundle: build/ACECode.app" >&2
+ exit 1
+ fi
+ cp -r "build/ACECode.app" "$package_dir/"
+ else
+ if [ ! -f "build/acecode-desktop" ]; then
+ echo "Missing desktop executable: build/acecode-desktop" >&2
+ exit 1
+ fi
+ cp "build/acecode-desktop" "$package_dir/"
+ cp "build/acecode-logo.png" "$package_dir/"
+ fi
+ fi
+ legacy="$(find "$package_dir" -maxdepth 1 -iname 'ace-browser-*' -print -quit)"
+ if [ -n "$legacy" ]; then
+ echo "Legacy browser artifact must not be packaged: $legacy" >&2
+ exit 1
+ fi
+ tar -C dist -czf "acecode-${{ matrix.id }}.tar.gz" "acecode-${{ matrix.id }}"
+
+ verify_root="$(mktemp -d)"
+ tar -xzf "acecode-${{ matrix.id }}.tar.gz" -C "$verify_root"
+ extracted_package="$verify_root/acecode-${{ matrix.id }}"
+ validate_models_dev_registry "$extracted_package/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/share/acecode/seed"
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ test -x "$extracted_package/acecode-computer-use"
+ test -x "$extracted_package/ACECode.app/Contents/MacOS/acecode-computer-use"
+ validate_models_dev_registry \
+ "$extracted_package/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/ACECode.app/Contents/Resources/share/acecode/seed"
+ fi
+ rm -rf -- "$verify_root"
+ - name: Create Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ id: linux-update
+ shell: bash
+ run: |
+ update_path="acecode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update.zip"
+ bash scripts/linux_create_update_zip.sh \
+ --package-dir "dist/acecode-${{ matrix.id }}" \
+ --output "$update_path" \
+ --expected-version "${{ steps.package-version.outputs.version }}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Clean up macOS signing material
+ if: always() && runner.os == 'macOS'
+ shell: bash
+ run: |
+ set +e
+ security delete-keychain "$RUNNER_TEMP/acecode-signing.keychain-db" 2>/dev/null
+ rm -f -- \
+ "$RUNNER_TEMP/acecode-developer-id.p12" \
+ "$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ - name: Package (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
+
+ function Assert-ModelsDevRegistry([string] $RegistryDir) {
+ if (-not (Test-Path -LiteralPath $RegistryDir -PathType Container)) {
+ throw "Missing models.dev directory: $RegistryDir"
+ }
+ $actualFiles = @(Get-ChildItem -LiteralPath $RegistryDir -File -Recurse)
+ if ($actualFiles.Count -ne 3) {
+ throw "Expected exactly 3 models.dev files in ${RegistryDir}, found $($actualFiles.Count)"
+ }
+ foreach ($name in @("api.json", "MANIFEST.json", "LICENSE")) {
+ $source = Join-Path "assets/models_dev" $name
+ $packaged = Join-Path $RegistryDir $name
+ if (-not (Test-Path -LiteralPath $packaged -PathType Leaf)) {
+ throw "Missing models.dev package file: $packaged"
+ }
+ $sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
+ $packagedHash = (Get-FileHash -LiteralPath $packaged -Algorithm SHA256).Hash
+ if ($sourceHash -ne $packagedHash) {
+ throw "Mismatched models.dev package file: $packaged"
+ }
+ }
+ }
+
+ $terminalExe = "build/${{ matrix.executable }}"
+ if (-not (Test-Path -LiteralPath $terminalExe)) {
+ throw "Missing terminal executable: $terminalExe"
+ }
+ Copy-Item -LiteralPath $terminalExe -Destination "$packageDir/"
+ Copy-Item "README.md","README_CN.md" "$packageDir/"
+ $packageRoot = (Resolve-Path -LiteralPath $packageDir).Path
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component models_dev_registry
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the bundled models.dev registry"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component default_seed_bundle
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the default seed bundle"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component whatsapp_channel
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the WhatsApp channel bridge"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component computer_use_runtime
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the Computer Use runtime"
+ }
+ $computerUseExe = Join-Path $packageRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $computerUseExe -PathType Leaf) -or
+ (Get-Item -LiteralPath $computerUseExe).Length -eq 0) {
+ throw "Missing Computer Use runtime: $computerUseExe"
+ }
+ Assert-ModelsDevRegistry (Join-Path $packageDir "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $packageDir "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the packaged default seed bundle"
+ }
+
+ if ("${{ matrix.build_desktop }}" -eq "true") {
+ $desktopCandidates = @(
+ "build/acecode-desktop.exe",
+ "build/Release/acecode-desktop.exe",
+ "build/Debug/acecode-desktop.exe",
+ "build/RelWithDebInfo/acecode-desktop.exe",
+ "build/MinSizeRel/acecode-desktop.exe"
+ )
+ $desktopExe = $desktopCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
+ if (-not $desktopExe) {
+ $desktopExe = Get-ChildItem -Path "build" -Recurse -Filter "acecode-desktop.exe" -File |
+ Select-Object -ExpandProperty FullName -First 1
+ }
+ if (-not $desktopExe) {
+ throw "Missing desktop executable: acecode-desktop.exe"
+ }
+ Copy-Item -LiteralPath $desktopExe -Destination "$packageDir/"
+ }
+ $legacyBrowserArtifact = Get-ChildItem -LiteralPath $packageDir -Filter "ace-browser-*" -Force |
+ Select-Object -First 1
+ if ($legacyBrowserArtifact) {
+ throw "Legacy browser artifact must not be packaged: $($legacyBrowserArtifact.FullName)"
+ }
+ Compress-Archive -Path "$packageDir/*" -DestinationPath "acecode-${{ matrix.id }}.zip" -Force
+
+ $verifyRoot = Join-Path $env:RUNNER_TEMP ("acecode-package-verify-" + [Guid]::NewGuid().ToString("N"))
+ New-Item -ItemType Directory -Path $verifyRoot | Out-Null
+ Expand-Archive -LiteralPath "acecode-${{ matrix.id }}.zip" -DestinationPath $verifyRoot
+ $archivedComputerUseExe = Join-Path $verifyRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $archivedComputerUseExe -PathType Leaf) -or
+ (Get-FileHash -LiteralPath $archivedComputerUseExe -Algorithm SHA256).Hash -ne
+ (Get-FileHash -LiteralPath $computerUseExe -Algorithm SHA256).Hash) {
+ throw "Archived Computer Use runtime is missing or does not match the build"
+ }
+ Assert-ModelsDevRegistry (Join-Path $verifyRoot "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $verifyRoot "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the archived default seed bundle"
+ }
+ - name: Verify Windows ARM64 package
+ if: matrix.id == 'windows-arm64'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ $expectedExecutables = @(
+ "acecode.exe",
+ "acecode-desktop.exe",
+ "acecode-computer-use.exe"
+ )
+ foreach ($name in $expectedExecutables) {
+ $path = Join-Path $packageDir $name
+ if (-not (Test-Path -LiteralPath $path)) {
+ throw "Missing ARM64 package executable: $path"
+ }
+ $headers = (& dumpbin /headers $path | Out-String)
+ if ($LASTEXITCODE -ne 0 -or $headers -notmatch '(?im)\bAA64 machine\b') {
+ throw "$name is not an ARM64 PE executable"
+ }
+ Write-Host "${name}: ARM64"
+ }
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}
+ path: acecode-${{ matrix.id }}.${{ matrix.archive_extension }}
+ if-no-files-found: error
+ - name: Upload Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.linux-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dmg
+ path: ${{ steps.macos-dmg.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS self-update archive
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.macos-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload PDB
+ if: runner.os == 'Windows'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pdb
+ path: build/**/acecode.pdb
+ if-no-files-found: warn
+ - name: Upload desktop PDB
+ if: runner.os == 'Windows' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-pdb
+ path: build/**/acecode-desktop.pdb
+ if-no-files-found: warn
+ - name: Upload debug symbols (Linux)
+ if: runner.os == 'Linux'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-debug
+ path: build/${{ matrix.executable }}.debug
+ if-no-files-found: warn
+ - name: Upload desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-debug
+ path: build/acecode-desktop.debug
+ if-no-files-found: warn
+ - name: Upload dSYM (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dsym
+ path: build/${{ matrix.executable }}.dSYM
+ if-no-files-found: warn
+ - name: Upload desktop dSYM (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-dsym
+ path: |
+ build/ACECode.app.dSYM
+ build/acecode-daemon.dSYM
+ if-no-files-found: warn
+ timeout-minutes: 90
diff --git a/.github/workflows/recover-v1.0.7-platform-assets.yml b/.github/workflows/recover-v1.0.7-platform-assets.yml
new file mode 100644
index 000000000..f6bbfac7f
--- /dev/null
+++ b/.github/workflows/recover-v1.0.7-platform-assets.yml
@@ -0,0 +1,919 @@
+name: Recover v1.0.7 macOS x64 assets
+'on':
+ push:
+ branches:
+ - claude/recover-v1.0.7-platform-assets
+permissions:
+ contents: read
+ actions: read
+concurrency:
+ group: recover-v1.0.7-macos-x64
+ cancel-in-progress: false
+jobs:
+ build-web-ui:
+ name: web-dist
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: v1.0.7
+ - name: Verify immutable v1.0.7 source
+ shell: bash
+ run: |
+ test "$(git rev-parse HEAD)" = "bb779f1f81e55536e56d21c3e5d66be1b2c1158e"
+ git diff --exit-code
+ - name: Check bundled models.dev freshness for release
+ if: 'true'
+ shell: bash
+ run: |
+ python3 - <<'PY'
+ import datetime as dt
+ import json
+ from pathlib import Path
+
+ manifest_path = Path("assets/models_dev/MANIFEST.json")
+ try:
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ generated_at = dt.datetime.fromisoformat(
+ manifest["generated_at"].replace("Z", "+00:00")
+ )
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
+ raise SystemExit(f"Invalid {manifest_path}: {exc}")
+
+ if generated_at.tzinfo is None:
+ raise SystemExit(f"{manifest_path}: generated_at must include a timezone")
+ now = dt.datetime.now(dt.timezone.utc)
+ age = now - generated_at.astimezone(dt.timezone.utc)
+ if age < -dt.timedelta(minutes=5):
+ raise SystemExit(f"{manifest_path}: generated_at is in the future")
+ if age > dt.timedelta(days=30):
+ raise SystemExit(
+ f"Bundled models.dev registry is {age.days} days old; "
+ "run scripts/sync_models_dev before creating a release tag"
+ )
+ print(f"Bundled models.dev registry age: {age.days} days")
+ PY
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.32.1
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: pnpm
+ cache-dependency-path: web/pnpm-lock.yaml
+ - name: Build web UI
+ shell: bash
+ run: |
+ cd web
+ pnpm install --frozen-lockfile
+ pnpm build
+ - name: Upload web UI
+ uses: actions/upload-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ if-no-files-found: error
+ timeout-minutes: 60
+ env:
+ ACE_RECOVERY_REF: refs/tags/v1.0.7
+ ACE_RECOVERY_REF_NAME: v1.0.7
+ build-and-package:
+ name: ${{ matrix.id }}
+ needs: build-web-ui
+ runs-on: ${{ matrix.os }}
+ env:
+ VCPKG_BINARY_SOURCES: clear;default,readwrite
+ ACE_RECOVERY_REF: refs/tags/v1.0.7
+ ACE_RECOVERY_REF_NAME: v1.0.7
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - id: macos-x64
+ os: macos-15
+ installer_arch: x64
+ triplet: x64-osx
+ executable: acecode
+ archive_extension: tar.gz
+ build_desktop: true
+ cmake_extra_args: -DCMAKE_OSX_ARCHITECTURES=x86_64 -DACECODE_BUILD_DESKTOP=ON
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ ref: v1.0.7
+ - name: Verify immutable v1.0.7 source
+ shell: bash
+ run: |
+ test "$(git rev-parse HEAD)" = "bb779f1f81e55536e56d21c3e5d66be1b2c1158e"
+ git diff --exit-code
+ - name: Resolve package version
+ id: package-version
+ shell: bash
+ run: |
+ declared_version="$(awk '$1 == "project(acecode" && $2 == "VERSION" { print $3; exit }' CMakeLists.txt)"
+ if [ -z "$declared_version" ]; then
+ echo "::error::Unable to read the ACECode version from CMakeLists.txt"
+ exit 1
+ fi
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* && "${ACE_RECOVERY_REF_NAME#v}" != "$declared_version" ]]; then
+ echo "::error::Tag $ACE_RECOVERY_REF_NAME does not match CMake version $declared_version"
+ exit 1
+ fi
+ echo "version=$declared_version" >> "$GITHUB_OUTPUT"
+ - name: Check macOS release credentials
+ if: runner.os == 'macOS'
+ id: macos-release
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ application_missing=()
+ for name in \
+ MACOS_CERTIFICATE_BASE64 \
+ MACOS_CERTIFICATE_PASSWORD \
+ APPLE_ID \
+ APPLE_TEAM_ID \
+ APPLE_APP_SPECIFIC_PASSWORD; do
+ if [ -z "${!name}" ]; then
+ application_missing+=("$name")
+ fi
+ done
+
+ if [ "${#application_missing[@]}" -ne 0 ]; then
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS application releases require secrets: ${application_missing[*]}"
+ exit 1
+ fi
+ echo "::notice::macOS signing/notarization disabled; missing: ${application_missing[*]}"
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ installer_certificate_set=false
+ installer_password_set=false
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_BASE64" ]; then
+ installer_certificate_set=true
+ fi
+ if [ -n "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" ]; then
+ installer_password_set=true
+ fi
+
+ if [ "$installer_certificate_set" != "$installer_password_set" ]; then
+ echo "::error::macOS Installer credentials must configure both MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD"
+ exit 1
+ fi
+ if [ "$installer_certificate_set" = "true" ]; then
+ if [ "${#application_missing[@]}" -eq 0 ]; then
+ echo "pkg_enabled=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "::notice::macOS PKG creation disabled because application signing/notarization is disabled"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ if [[ "$ACE_RECOVERY_REF" == refs/tags/v* ]]; then
+ echo "::error::Tagged macOS releases require MACOS_INSTALLER_CERTIFICATE_BASE64 and MACOS_INSTALLER_CERTIFICATE_PASSWORD to build both PKGs"
+ exit 1
+ fi
+ echo "::notice::macOS PKG creation disabled; Developer ID Installer credentials are not configured"
+ echo "pkg_enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Configure MSVC
+ if: runner.os == 'Windows'
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ matrix.msvc_arch }}
+ - name: Install Linux ARMv7 cross toolchain
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ binutils-arm-linux-gnueabihf \
+ gcc-arm-linux-gnueabihf \
+ g++-arm-linux-gnueabihf \
+ qemu-user
+ - name: Run vcpkg
+ uses: lukka/run-vcpkg@v11
+ - name: Restore vcpkg binary cache
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
+ key: vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**')
+ }}
+ restore-keys: |
+ vcpkg-v2-${{ matrix.os }}-${{ matrix.triplet }}-
+ - name: Install Linux desktop dependencies
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev
+ - name: Download web UI
+ uses: actions/download-artifact@v4
+ with:
+ name: web-dist
+ path: web/dist
+ - name: Configure
+ shell: bash
+ run: |
+ cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=MinSizeRel \
+ -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
+ -DVCPKG_TARGET_TRIPLET="${{ matrix.triplet }}" \
+ -DBUILD_TESTING=OFF \
+ ${{ matrix.cmake_extra_args }}
+ - name: Build
+ shell: bash
+ run: cmake --build build --config MinSizeRel --target acecode
+ - name: Build desktop
+ if: matrix.build_desktop
+ shell: bash
+ run: cmake --build build --config MinSizeRel --target acecode-desktop
+ - name: Verify Linux ARMv7 executable
+ if: matrix.id == 'linux-armv7'
+ shell: bash
+ run: |
+ export LC_ALL=C
+ elf_header="$(arm-linux-gnueabihf-readelf -h build/acecode)"
+ arm_attributes="$(arm-linux-gnueabihf-readelf -A build/acecode)"
+ program_headers="$(arm-linux-gnueabihf-readelf -l build/acecode)"
+
+ printf '%s\n' "$elf_header"
+ printf '%s\n' "$arm_attributes"
+
+ grep -Eq 'Class:[[:space:]]+ELF32' <<<"$elf_header"
+ grep -Eq 'Machine:[[:space:]]+ARM' <<<"$elf_header"
+ grep -Eq 'Tag_CPU_arch:[[:space:]]+v7' <<<"$arm_attributes"
+ grep -Eq 'Tag_ABI_VFP_args:[[:space:]]+VFP registers' <<<"$arm_attributes"
+ grep -Fq '/lib/ld-linux-armhf.so.3' <<<"$program_headers"
+
+ version_output="$(qemu-arm -L /usr/arm-linux-gnueabihf build/acecode --version)"
+ printf '%s\n' "$version_output"
+ grep -Eq '^acecode v[^[:space:]]+$' <<<"$version_output"
+ - name: Verify bundled resources in macOS app
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ registry_dir="build/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev resources in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched macOS app models.dev resource: $file" >&2
+ exit 1
+ fi
+ done
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged build/ACECode.app/Contents/Resources/share/acecode/seed
+ - name: Extract debug symbols (Linux)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ binutils_prefix=""
+ if [ "${{ matrix.id }}" = "linux-armv7" ]; then
+ binutils_prefix="arm-linux-gnueabihf-"
+ fi
+ "${binutils_prefix}objcopy" --only-keep-debug \
+ "build/${{ matrix.executable }}" \
+ "build/${{ matrix.executable }}.debug"
+ "${binutils_prefix}strip" --strip-unneeded "build/${{ matrix.executable }}"
+ "${binutils_prefix}objcopy" \
+ --add-gnu-debuglink="build/${{ matrix.executable }}.debug" \
+ "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ shell: bash
+ run: |
+ objcopy --only-keep-debug "build/acecode-desktop" "build/acecode-desktop.debug"
+ strip --strip-unneeded "build/acecode-desktop"
+ objcopy --add-gnu-debuglink="build/acecode-desktop.debug" "build/acecode-desktop"
+ - name: Extract debug symbols (macOS)
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ dsymutil "build/${{ matrix.executable }}" -o "build/${{ matrix.executable }}.dSYM"
+ strip "build/${{ matrix.executable }}"
+ - name: Extract desktop debug symbols (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ shell: bash
+ run: |
+ app_exec="build/ACECode.app/Contents/MacOS/ACECode"
+ app_daemon="build/ACECode.app/Contents/MacOS/acecode-daemon"
+ if [ ! -f "$app_exec" ]; then
+ echo "Missing desktop app executable: $app_exec" >&2
+ exit 1
+ fi
+ if [ ! -f "$app_daemon" ]; then
+ echo "Missing desktop app daemon: $app_daemon" >&2
+ exit 1
+ fi
+ dsymutil "$app_exec" -o "build/ACECode.app.dSYM"
+ strip "$app_exec"
+ dsymutil "$app_daemon" -o "build/acecode-daemon.dSYM"
+ strip "$app_daemon"
+ - name: Import Developer ID identities (macOS)
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-keychain
+ shell: bash
+ env:
+ MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ MACOS_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_BASE64 }}
+ MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ MACOS_CODESIGN_IDENTITY: ${{ vars.MACOS_CODESIGN_IDENTITY }}
+ MACOS_INSTALLER_SIGNING_IDENTITY: ${{ vars.MACOS_INSTALLER_SIGNING_IDENTITY }}
+ run: |
+ set +x
+ certificate_path="$RUNNER_TEMP/acecode-developer-id.p12"
+ installer_certificate_path="$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ keychain_path="$RUNNER_TEMP/acecode-signing.keychain-db"
+ keychain_password="$(openssl rand -hex 32)"
+
+ printf '%s' "$MACOS_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$certificate_path"
+ chmod 600 "$certificate_path"
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ printf '%s' "$MACOS_INSTALLER_CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$installer_certificate_path"
+ chmod 600 "$installer_certificate_path"
+ fi
+ security create-keychain -p "$keychain_password" "$keychain_path"
+ security set-keychain-settings -lut 21600 "$keychain_path"
+ security unlock-keychain -p "$keychain_password" "$keychain_path"
+
+ existing_user_keychains=()
+ while IFS= read -r existing_keychain; do
+ if [ -n "$existing_keychain" ]; then
+ existing_user_keychains+=("$existing_keychain")
+ fi
+ done < <(
+ security list-keychains -d user |
+ sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//'
+ )
+ security list-keychains -d user -s \
+ "$keychain_path" \
+ "${existing_user_keychains[@]}"
+
+ security import "$certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/codesign \
+ -T /usr/bin/security
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ security import "$installer_certificate_path" \
+ -k "$keychain_path" \
+ -P "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" \
+ -f pkcs12 \
+ -T /usr/bin/pkgbuild \
+ -T /usr/bin/productbuild \
+ -T /usr/bin/security
+ fi
+ security set-key-partition-list \
+ -S apple-tool:,apple: \
+ -s \
+ -k "$keychain_password" \
+ "$keychain_path"
+
+ identity_record="$(security find-identity -v -p codesigning "$keychain_path" |
+ awk '/Developer ID Application:/ { print; exit }')"
+ identity_fingerprint="$(printf '%s\n' "$identity_record" | awk '{ print $2 }')"
+ identity_name="$(printf '%s\n' "$identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$identity_name" ]; then
+ echo "::error::No Developer ID Application identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_CODESIGN_IDENTITY" ] && [ "$identity_name" != "$MACOS_CODESIGN_IDENTITY" ]; then
+ echo "::error::Imported signing identity does not match MACOS_CODESIGN_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Application identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+
+ if [ "${{ steps.macos-release.outputs.pkg_enabled }}" = "true" ]; then
+ installer_identity_record="$(security find-identity -v -p basic "$keychain_path" |
+ awk '/Developer ID Installer:/ { print; exit }')"
+ installer_identity_fingerprint="$(printf '%s\n' "$installer_identity_record" | awk '{ print $2 }')"
+ installer_identity_name="$(printf '%s\n' "$installer_identity_record" | awk -F '"' '{ print $2 }')"
+ if [[ ! "$installer_identity_fingerprint" =~ ^[[:xdigit:]]{40}$ ]] || [ -z "$installer_identity_name" ]; then
+ echo "::error::No Developer ID Installer identity found in the imported certificate"
+ exit 1
+ fi
+ if [ -n "$MACOS_INSTALLER_SIGNING_IDENTITY" ] && [ "$installer_identity_name" != "$MACOS_INSTALLER_SIGNING_IDENTITY" ]; then
+ echo "::error::Imported Installer identity does not match MACOS_INSTALLER_SIGNING_IDENTITY"
+ exit 1
+ fi
+ if [ -n "$APPLE_TEAM_ID" ] && [[ "$installer_identity_name" != *"($APPLE_TEAM_ID)" ]]; then
+ echo "::error::Developer ID Installer identity does not match APPLE_TEAM_ID"
+ exit 1
+ fi
+ echo "installer_identity=$installer_identity_name" >> "$GITHUB_OUTPUT"
+ fi
+
+ echo "keychain=$keychain_path" >> "$GITHUB_OUTPUT"
+ # Sign by the certificate fingerprint. codesign can fail to resolve a
+ # human-readable identity name inside an isolated temporary keychain.
+ echo "identity=$identity_fingerprint" >> "$GITHUB_OUTPUT"
+ - name: Sign macOS release payloads
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ scripts/macos_codesign.sh \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}" \
+ --binary "build/${{ matrix.executable }}" \
+ --binary "build/acecode-computer-use" \
+ --app "build/ACECode.app"
+ - name: Notarize and staple macOS app
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ scripts/macos_notarize_app.sh --app "build/ACECode.app"
+ - name: Test macOS installer integration
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ bash tests/scripts/macos_installer_test.sh
+ python3 tests/scripts/macos_installer_release_test.py
+ - name: Create current-user macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ id: macos-pkg
+ shell: bash
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ pkg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.pkg"
+ scripts/macos_create_pkg.sh \
+ --app "build/ACECode.app" \
+ --output "$pkg_path" \
+ --installer-identity "${{ steps.macos-keychain.outputs.installer_identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$pkg_path" >> "$GITHUB_OUTPUT"
+ - name: Notarize and validate macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |-
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ scripts/macos_notarize_pkg.sh --pkg "${{ steps.macos-pkg.outputs.path }}"
+ - name: Upload macOS PKG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.pkg_enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pkg
+ path: ${{ steps.macos-pkg.outputs.path }}
+ if-no-files-found: error
+ - name: Build signed and notarized macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ id: macos-dmg
+ shell: bash
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ dmg_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}.dmg"
+ bash scripts/macos_create_installer_dmg.sh \
+ --app "build/ACECode.app" \
+ --output "$dmg_path" \
+ --arch "${{ matrix.installer_arch }}" \
+ --identity "${{ steps.macos-keychain.outputs.identity }}" \
+ --keychain "${{ steps.macos-keychain.outputs.keychain }}"
+ echo "path=$dmg_path" >> "$GITHUB_OUTPUT"
+ - name: Create macOS self-update archive
+ if: runner.os == 'macOS'
+ id: macos-update
+ shell: bash
+ run: |
+ xcrun() {
+ if [[ "${1:-}" != "stapler" ]]; then
+ command xcrun "$@"
+ return $?
+ fi
+ local result=0
+ for attempt in 1 2 3 4 5 6; do
+ if command xcrun "$@"; then return 0; else result=$?; fi
+ if [[ "$result" != "68" || "$attempt" == "6" ]]; then return "$result"; fi
+ echo "Apple ticket service disconnected (68), retry $attempt/6 in 15 seconds"
+ sleep 15
+ done
+ return "$result"
+ }
+ export -f xcrun
+ unsigned_suffix=""
+ trust_args=()
+ if [ "${{ steps.macos-release.outputs.enabled }}" = "true" ]; then
+ trust_args+=(--require-trusted)
+ else
+ unsigned_suffix="-unsigned"
+ fi
+ update_path="ACECode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update${unsigned_suffix}.zip"
+ scripts/macos_create_update_zip.sh \
+ --app "build/ACECode.app" \
+ --output "$update_path" \
+ "${trust_args[@]}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Package (Unix)
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ package_dir="dist/acecode-${{ matrix.id }}"
+ mkdir -p "$package_dir"
+
+ validate_models_dev_registry() {
+ registry_dir="$1"
+ file_count="$(find "$registry_dir" -type f | wc -l | tr -d '[:space:]')"
+ if [ "$file_count" != "3" ]; then
+ echo "Expected exactly 3 models.dev files in $registry_dir, found $file_count" >&2
+ exit 1
+ fi
+ for file in api.json MANIFEST.json LICENSE; do
+ if [ ! -f "$registry_dir/$file" ] ||
+ ! cmp -s "assets/models_dev/$file" "$registry_dir/$file"; then
+ echo "Missing or mismatched models.dev package file: $registry_dir/$file" >&2
+ exit 1
+ fi
+ done
+ }
+
+ if [ ! -f "build/${{ matrix.executable }}" ]; then
+ echo "Missing terminal executable: build/${{ matrix.executable }}" >&2
+ exit 1
+ fi
+ cp "build/${{ matrix.executable }}" "$package_dir/"
+ cp README.md README_CN.md "$package_dir/"
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component models_dev_registry
+ cmake --install build \
+ --config MinSizeRel \
+ --prefix "$PWD/$package_dir" \
+ --component default_seed_bundle
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component whatsapp_channel
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ cmake --install build --config MinSizeRel --prefix "$PWD/$package_dir" --component computer_use_runtime
+ test -x "$package_dir/acecode-computer-use"
+ test -x "build/ACECode.app/Contents/MacOS/acecode-computer-use"
+ fi
+ validate_models_dev_registry "$package_dir/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$package_dir/share/acecode/seed"
+
+ if [ "${{ matrix.build_desktop }}" = "true" ]; then
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ if [ ! -d "build/ACECode.app" ]; then
+ echo "Missing desktop app bundle: build/ACECode.app" >&2
+ exit 1
+ fi
+ cp -r "build/ACECode.app" "$package_dir/"
+ else
+ if [ ! -f "build/acecode-desktop" ]; then
+ echo "Missing desktop executable: build/acecode-desktop" >&2
+ exit 1
+ fi
+ cp "build/acecode-desktop" "$package_dir/"
+ cp "build/acecode-logo.png" "$package_dir/"
+ fi
+ fi
+ legacy="$(find "$package_dir" -maxdepth 1 -iname 'ace-browser-*' -print -quit)"
+ if [ -n "$legacy" ]; then
+ echo "Legacy browser artifact must not be packaged: $legacy" >&2
+ exit 1
+ fi
+ tar -C dist -czf "acecode-${{ matrix.id }}.tar.gz" "acecode-${{ matrix.id }}"
+
+ verify_root="$(mktemp -d)"
+ tar -xzf "acecode-${{ matrix.id }}.tar.gz" -C "$verify_root"
+ extracted_package="$verify_root/acecode-${{ matrix.id }}"
+ validate_models_dev_registry "$extracted_package/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/share/acecode/seed"
+ if [ "$RUNNER_OS" = "macOS" ]; then
+ test -x "$extracted_package/acecode-computer-use"
+ test -x "$extracted_package/ACECode.app/Contents/MacOS/acecode-computer-use"
+ validate_models_dev_registry \
+ "$extracted_package/ACECode.app/Contents/Resources/share/acecode/models_dev"
+ python3 scripts/verify_seed_bundle.py \
+ --source assets/seed \
+ --packaged "$extracted_package/ACECode.app/Contents/Resources/share/acecode/seed"
+ fi
+ rm -rf -- "$verify_root"
+ - name: Create Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ id: linux-update
+ shell: bash
+ run: |
+ update_path="acecode-${{ steps.package-version.outputs.version }}-${{ matrix.id }}-update.zip"
+ bash scripts/linux_create_update_zip.sh \
+ --package-dir "dist/acecode-${{ matrix.id }}" \
+ --output "$update_path" \
+ --expected-version "${{ steps.package-version.outputs.version }}"
+ echo "path=$update_path" >> "$GITHUB_OUTPUT"
+ - name: Clean up macOS signing material
+ if: always() && runner.os == 'macOS'
+ shell: bash
+ run: |
+ set +e
+ security delete-keychain "$RUNNER_TEMP/acecode-signing.keychain-db" 2>/dev/null
+ rm -f -- \
+ "$RUNNER_TEMP/acecode-developer-id.p12" \
+ "$RUNNER_TEMP/acecode-developer-id-installer.p12"
+ - name: Package (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
+
+ function Assert-ModelsDevRegistry([string] $RegistryDir) {
+ if (-not (Test-Path -LiteralPath $RegistryDir -PathType Container)) {
+ throw "Missing models.dev directory: $RegistryDir"
+ }
+ $actualFiles = @(Get-ChildItem -LiteralPath $RegistryDir -File -Recurse)
+ if ($actualFiles.Count -ne 3) {
+ throw "Expected exactly 3 models.dev files in ${RegistryDir}, found $($actualFiles.Count)"
+ }
+ foreach ($name in @("api.json", "MANIFEST.json", "LICENSE")) {
+ $source = Join-Path "assets/models_dev" $name
+ $packaged = Join-Path $RegistryDir $name
+ if (-not (Test-Path -LiteralPath $packaged -PathType Leaf)) {
+ throw "Missing models.dev package file: $packaged"
+ }
+ $sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
+ $packagedHash = (Get-FileHash -LiteralPath $packaged -Algorithm SHA256).Hash
+ if ($sourceHash -ne $packagedHash) {
+ throw "Mismatched models.dev package file: $packaged"
+ }
+ }
+ }
+
+ $terminalExe = "build/${{ matrix.executable }}"
+ if (-not (Test-Path -LiteralPath $terminalExe)) {
+ throw "Missing terminal executable: $terminalExe"
+ }
+ Copy-Item -LiteralPath $terminalExe -Destination "$packageDir/"
+ Copy-Item "README.md","README_CN.md" "$packageDir/"
+ $packageRoot = (Resolve-Path -LiteralPath $packageDir).Path
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component models_dev_registry
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the bundled models.dev registry"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component default_seed_bundle
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the default seed bundle"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component whatsapp_channel
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the WhatsApp channel bridge"
+ }
+ & cmake --install build --config MinSizeRel --prefix $packageRoot --component computer_use_runtime
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install the Computer Use runtime"
+ }
+ $computerUseExe = Join-Path $packageRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $computerUseExe -PathType Leaf) -or
+ (Get-Item -LiteralPath $computerUseExe).Length -eq 0) {
+ throw "Missing Computer Use runtime: $computerUseExe"
+ }
+ Assert-ModelsDevRegistry (Join-Path $packageDir "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $packageDir "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the packaged default seed bundle"
+ }
+
+ if ("${{ matrix.build_desktop }}" -eq "true") {
+ $desktopCandidates = @(
+ "build/acecode-desktop.exe",
+ "build/Release/acecode-desktop.exe",
+ "build/Debug/acecode-desktop.exe",
+ "build/RelWithDebInfo/acecode-desktop.exe",
+ "build/MinSizeRel/acecode-desktop.exe"
+ )
+ $desktopExe = $desktopCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
+ if (-not $desktopExe) {
+ $desktopExe = Get-ChildItem -Path "build" -Recurse -Filter "acecode-desktop.exe" -File |
+ Select-Object -ExpandProperty FullName -First 1
+ }
+ if (-not $desktopExe) {
+ throw "Missing desktop executable: acecode-desktop.exe"
+ }
+ Copy-Item -LiteralPath $desktopExe -Destination "$packageDir/"
+ }
+ $legacyBrowserArtifact = Get-ChildItem -LiteralPath $packageDir -Filter "ace-browser-*" -Force |
+ Select-Object -First 1
+ if ($legacyBrowserArtifact) {
+ throw "Legacy browser artifact must not be packaged: $($legacyBrowserArtifact.FullName)"
+ }
+ Compress-Archive -Path "$packageDir/*" -DestinationPath "acecode-${{ matrix.id }}.zip" -Force
+
+ $verifyRoot = Join-Path $env:RUNNER_TEMP ("acecode-package-verify-" + [Guid]::NewGuid().ToString("N"))
+ New-Item -ItemType Directory -Path $verifyRoot | Out-Null
+ Expand-Archive -LiteralPath "acecode-${{ matrix.id }}.zip" -DestinationPath $verifyRoot
+ $archivedComputerUseExe = Join-Path $verifyRoot "acecode-computer-use.exe"
+ if (-not (Test-Path -LiteralPath $archivedComputerUseExe -PathType Leaf) -or
+ (Get-FileHash -LiteralPath $archivedComputerUseExe -Algorithm SHA256).Hash -ne
+ (Get-FileHash -LiteralPath $computerUseExe -Algorithm SHA256).Hash) {
+ throw "Archived Computer Use runtime is missing or does not match the build"
+ }
+ Assert-ModelsDevRegistry (Join-Path $verifyRoot "share/acecode/models_dev")
+ & python scripts/verify_seed_bundle.py `
+ --source assets/seed `
+ --packaged (Join-Path $verifyRoot "share/acecode/seed")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to verify the archived default seed bundle"
+ }
+ - name: Verify Windows ARM64 package
+ if: matrix.id == 'windows-arm64'
+ shell: pwsh
+ run: |
+ $packageDir = "dist/acecode-${{ matrix.id }}"
+ $expectedExecutables = @(
+ "acecode.exe",
+ "acecode-desktop.exe",
+ "acecode-computer-use.exe"
+ )
+ foreach ($name in $expectedExecutables) {
+ $path = Join-Path $packageDir $name
+ if (-not (Test-Path -LiteralPath $path)) {
+ throw "Missing ARM64 package executable: $path"
+ }
+ $headers = (& dumpbin /headers $path | Out-String)
+ if ($LASTEXITCODE -ne 0 -or $headers -notmatch '(?im)\bAA64 machine\b') {
+ throw "$name is not an ARM64 PE executable"
+ }
+ Write-Host "${name}: ARM64"
+ }
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}
+ path: acecode-${{ matrix.id }}.${{ matrix.archive_extension }}
+ if-no-files-found: error
+ - name: Upload Linux self-update archive
+ if: matrix.id == 'linux-x64' || matrix.id == 'linux-arm64'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.linux-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS installer DMG
+ if: runner.os == 'macOS' && steps.macos-release.outputs.enabled == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dmg
+ path: ${{ steps.macos-dmg.outputs.path }}
+ if-no-files-found: error
+ - name: Upload macOS self-update archive
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-update
+ path: ${{ steps.macos-update.outputs.path }}
+ if-no-files-found: error
+ - name: Upload PDB
+ if: runner.os == 'Windows'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-pdb
+ path: build/**/acecode.pdb
+ if-no-files-found: warn
+ - name: Upload desktop PDB
+ if: runner.os == 'Windows' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-pdb
+ path: build/**/acecode-desktop.pdb
+ if-no-files-found: warn
+ - name: Upload debug symbols (Linux)
+ if: runner.os == 'Linux'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-debug
+ path: build/${{ matrix.executable }}.debug
+ if-no-files-found: warn
+ - name: Upload desktop debug symbols (Linux)
+ if: runner.os == 'Linux' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-debug
+ path: build/acecode-desktop.debug
+ if-no-files-found: warn
+ - name: Upload dSYM (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-dsym
+ path: build/${{ matrix.executable }}.dSYM
+ if-no-files-found: warn
+ - name: Upload desktop dSYM (macOS)
+ if: runner.os == 'macOS' && matrix.build_desktop
+ uses: actions/upload-artifact@v4
+ with:
+ name: acecode-${{ matrix.id }}-desktop-dsym
+ path: |
+ build/ACECode.app.dSYM
+ build/acecode-daemon.dSYM
+ if-no-files-found: warn
+ timeout-minutes: 60
diff --git a/assets/desktop_pet/agent_office_pet.html b/assets/desktop_pet/agent_office_pet.html
index 5e735c00a..b3c797e61 100644
--- a/assets/desktop_pet/agent_office_pet.html
+++ b/assets/desktop_pet/agent_office_pet.html
@@ -18,7 +18,8 @@
.ov{position:absolute;inset:0;pointer-events:none;line-height:1.4}
.scene canvas.up{position:absolute;inset:0;image-rendering:auto;pointer-events:none}
.grip{position:absolute;left:calc(11 / 344 * 100%);top:calc(112 / 252 * 100%);width:16px;height:16px;cursor:nwse-resize;opacity:0;transition:opacity .15s;z-index:3}
-.scene:hover .grip{opacity:.95}
+/* The resize handle stays hidden (but hit-testable) until the pointer is on it. */
+.grip:hover{opacity:.95}
.grip svg{display:block;width:100%;height:100%}
.zoom-badge{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#27324f;color:#fff;font-weight:700;border-radius:6px;padding:2px 9px;font-size:13px;pointer-events:none;z-index:4}
.bub{position:absolute;left:0;top:0;z-index:2;display:flex;align-items:center;gap:3px;max-width:min(64vw,calc(var(--k) * 150px));background:#fff;color:#1b2540;border:2px solid #27324f;border-radius:6px;padding:1px 6px 2px;font-size:max(10px,calc(var(--k) * 6.2px));font-weight:700;white-space:nowrap;box-shadow:2px 2px 0 rgba(20,24,40,.28)}
diff --git a/assets/desktop_pet/classic/README.md b/assets/desktop_pet/classic/README.md
index e0ad9a051..4ea450718 100644
--- a/assets/desktop_pet/classic/README.md
+++ b/assets/desktop_pet/classic/README.md
@@ -2,4 +2,8 @@
Clippy(43 个动作)与 Rover(29 个动作)原始 PNG 精灵图和动作数据来自 [clippyjs/clippy.js](https://github.com/clippyjs/clippy.js),固定提交和 SHA-256 见 `sources.json`。`preview.png` 直接裁取原图的 RestPose 首帧,仅用于设置中的预览。
+## 趴边偷看(clippy/peek.png、rover/peek.png)
+
+宠物拖到屏幕左/右边缘吸附后改用各自目录下的 `peek.png`:两帧并排(左睁眼、右闭眼),透明背景,画面右缘就是它躲着的那条屏幕边(贴左边时页面水平镜像)。不来自上游项目,2026-10-16 用 Codex 的内置图像生成(codex-cli 0.162)绘制,以本目录精灵图的整张图与 RestPose / Idle 首帧作为风格参考,要求同一角色设计与画风:回形针把上半截钢丝和两只大眼睛从画面右缘探进来,rover 探出脑袋、两只前爪扒着右缘。闭眼帧由同一张图做「只闭眼、其余不变」的编辑,再只把眼睛区域贴回睁眼图,眨眼时其余像素不动。处理:纯绿背景抠成透明并去绿边,两帧同裁,按眼睛大小与原帧一致缩到精灵分辨率(回形针 29×75、rover 41×56 每帧),alpha 只留全透明 / 不透明,颜色映射到对应 `map.png` 实际用到、且不偏绿的调色板颜色,所以和原帧一样是不平滑放大的像素画。换图时要按眼睛重新量缩放,页面里的倍数见 `classic_pet.js` 的 `PEEK` 表。
+
角色由 Microsoft 创作;上游代码许可保存在 `MIT-LICENSE.txt`。ACECode 自有播放器不依赖上游 jQuery 播放器,不播放原始音效。素材随本地程序打包,运行时没有 CDN 请求。
diff --git a/assets/desktop_pet/classic/clippy/peek.png b/assets/desktop_pet/classic/clippy/peek.png
new file mode 100644
index 000000000..9a0c54922
Binary files /dev/null and b/assets/desktop_pet/classic/clippy/peek.png differ
diff --git a/assets/desktop_pet/classic/rover/peek.png b/assets/desktop_pet/classic/rover/peek.png
new file mode 100644
index 000000000..9a382c6ec
Binary files /dev/null and b/assets/desktop_pet/classic/rover/peek.png differ
diff --git a/assets/desktop_pet/classic_pet.html b/assets/desktop_pet/classic_pet.html
index c28d2d78e..b205f13c3 100644
--- a/assets/desktop_pet/classic_pet.html
+++ b/assets/desktop_pet/classic_pet.html
@@ -21,7 +21,11 @@
box, and Windows clips painting to it, so a shadow running past it is cut off hard. */
/* Card text is the ACECode transcript body text: same family, size and line height (the main
window sends its live values with every snapshot, following its font-size setting). */
-.stack{position:absolute;left:0;right:0;bottom:calc(58% - 10px);z-index:3;padding:12px max(4%,16px) 26px;font:var(--pet-font-size)/1.6 var(--pet-font)}
+/* In a small window the cards would run past the top edge: --stack-fit (set by fitStack) shrinks
+ them, down to 9px text, and .pinned holds them at the top edge when even that is too tall. */
+.stack{position:absolute;left:0;right:0;bottom:calc(58% - 10px);z-index:3;padding:calc(12px * var(--stack-fit, 1)) max(4%,16px) calc(26px * var(--stack-fit, 1));
+ font:calc(var(--pet-font-size) * var(--stack-fit, 1))/1.6 var(--pet-font)}
+.stack.collapsed.pinned{bottom:auto;top:0}
.stack.expanded{bottom:auto;top:0;max-height:100%;overflow-y:auto;scrollbar-width:thin}
.deck{position:relative;isolation:isolate;display:flex;flex-direction:column;gap:6px}
.stack.collapsed .card:not(:first-child){display:none}
@@ -34,7 +38,8 @@
.text{flex:1;min-width:0}
.head{display:flex;align-items:center;gap:.4em;font-weight:700}
.name{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
-.body{margin-top:.1em;color:var(--card-muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
+/* A block box: on an inline span the ellipsis does nothing and long text runs under the stop button. */
+.body{display:block;margin-top:.1em;color:var(--card-muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.icon{flex:none;width:1.1em;height:1.1em;display:grid;place-items:center}
.icon svg{width:100%;height:100%}
.spinner{width:.9em;height:.9em;border:2px solid var(--card-button);border-top-color:var(--idle);border-radius:50%;animation:spin .9s linear infinite}
@@ -46,8 +51,10 @@
box-shadow:0 1px 3px rgba(0,0,0,.25);display:none;place-items:center;cursor:pointer;padding:0;font:inherit;font-weight:700;line-height:1}
.card:hover .dismiss,.card:focus-within .dismiss{display:grid}
.stack.collapsed.layers .card .dismiss{display:none}
-.grip{position:absolute;left:calc(11 / 344 * 100%);top:calc(112 / 252 * 100%);width:16px;height:16px;cursor:nwse-resize;opacity:0;z-index:2}
-.scene:hover .grip,.scene:focus-within .grip{opacity:1}
+/* The resize handle sits at the top-left of the visible pet (placed by reportRegions) and stays
+ hidden, though still hit-testable, until the pointer is on it. */
+.grip{position:absolute;width:16px;height:16px;cursor:nwse-resize;opacity:0;transition:opacity .15s;z-index:2}
+.grip:hover{opacity:1}
.grip svg{width:100%;height:100%;fill:none;stroke:var(--pet-ink);stroke-width:2}
.badge{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--pet-ink);color:#fff;padding:3px 8px;border-radius:5px;pointer-events:none}
[hidden]{display:none!important}
@@ -57,7 +64,7 @@
diff --git a/assets/desktop_pet/classic_pet.js b/assets/desktop_pet/classic_pet.js
index 1bc058e3b..fbeb6f8a5 100644
--- a/assets/desktop_pet/classic_pet.js
+++ b/assets/desktop_pet/classic_pet.js
@@ -52,14 +52,21 @@
sleep: 'sleep', wake: 'wake',
},
};
+ // Docked pets peek in from the screen edge. `hidden` canvas px of the art stays past the edge so
+ // they can lean in without revealing where the drawing is cut. `ratio` is how much larger the peek
+ // art is drawn than the sprite frames: it is shown at the frame scale divided by it, so the head
+ // keeps the size it has in every other pose instead of growing at the edge. Whale Girl's art is
+ // about 1.8x (eye height and headband-to-mouth both measure 1.8x); Clippy's and Rover's are made
+ // at sprite resolution (their eyes match the frames' eyes) and, like their frames, are scaled
+ // without smoothing. Their art is narrow, so less of it hides past the edge.
+ const PEEK = {
+ whale: { ratio: 1.8, hidden: 9, smooth: true },
+ clippy: { ratio: 1, hidden: 4, smooth: false },
+ rover: { ratio: 1, hidden: 4, smooth: false },
+ };
+ const peekStyle = () => PEEK[kind] || PEEK.whale;
// Extra hit margin (canvas px) so moving silhouettes stay clickable without per-frame region updates.
- // Docked Whale Girl peeks in from the screen edge; PEEK_HIDDEN of the art stays past the edge
- // so she can lean in without revealing where the drawing is cut.
- // The peek art is drawn about 1.8 times larger than the sprite frames (eye height and headband-to-
- // mouth both measure 1.8x), so it is shown at the frame scale divided by that ratio: her head
- // keeps the size it has in every other pose instead of nearly doubling at the edge.
- const PEEK_ART_RATIO = 1.8, PEEK_HIDDEN = 9;
- const MOTION_MARGIN = { float: [0, 4], breathe: [0, 3], shake: [3, 0], wiggle: [8, 0], tilt: [16, 2], peek: [PEEK_HIDDEN + 4, 8] };
+ const MOTION_MARGIN = { float: [0, 4], breathe: [0, 3], shake: [3, 0], wiggle: [8, 0], tilt: [16, 2] };
const timing = { fidgetMin: 7000, fidgetMax: 16000, sleepAfter: 5 * 60 * 1000, greeting: 6000 };
// The original agents hold a pose through self-looping branches (Clippy's idle stares ~6s).
// Once one picture has been shown this long, branches are skipped so the clip moves on.
@@ -68,8 +75,10 @@
let snapshot = { version: 1, follow: true, connected: false, agents: [], offices: [], selected: null, activities: [] };
let kind = '', data = null, sheet = null, profile = null, loadRevision = 0, pendingIntro = null;
let animation = '', frameIndex = 0, flipped = false, motionKind = '', images = null, imageKey = '', imageSince = 0;
- // Edge docking (Whale Girl only): the peek sheet, which edge, eyes open/closed, lean and bounce.
- let peek = null, docked = '', peekEyes = 0, peekLean = 0, peekBounce = '', peekBounceAt = 0;
+ // Edge docking (pets with peek art): the peek sheet, which edge, eyes open/closed, lean and bounce.
+ // `windowEdge` is the edge the native window sits on; it becomes `docked` once the pet has peek art,
+ // so a pet switched in (or still loading) while the window is docked peeks from the same edge.
+ let peek = null, docked = '', windowEdge = '', peekEyes = 0, peekLean = 0, peekBounce = '', peekBounceAt = 0;
let timer = 0, generation = 0, current = null;
let base = 'idle', variant = '', queue = [], stateKey = '';
// Session cards: keys already reacted to, cards the user dismissed, and whether the deck is spread out.
@@ -77,7 +86,8 @@
let dragging = false, dragDir = 0, dragPauseTimer = 0, dragWatchTimer = 0, dragEndedAt = 0;
let hovered = false, hoverOffTimer = 0, hoverReadyAt = 0, lastClickAt = 0;
let sleeping = false, lastActivity = performance.now(), fidgetAt = 0, fidgetTimer = 0, lastFidget = '';
- let greetingUntil = 0, greetingTimer = 0, rafId = 0, bodyRegions = [], spriteRect = null, bandCache = new Map();
+ let greetingUntil = 0, greetingTimer = 0, rafId = 0, bodyRegions = [], bodyBounds = null, bandCache = new Map();
+ let hoverPoint = null;
let overlayKey = '', badgeTimer = 0;
const pick = list => list[Math.floor(Math.random() * list.length)];
@@ -107,9 +117,10 @@
const [w, h] = data.framesize;
const frameScale = Math.min(184 / w, 140 / h);
if (peeking()) {
- const scale = frameScale / PEEK_ART_RATIO, width = peek.fw * scale, height = peek.fh * scale;
+ const { ratio, hidden } = peekStyle();
+ const scale = frameScale / ratio, width = peek.fw * scale, height = peek.fh * scale;
return { peek: true, w: peek.fw, h: peek.fh, scale, width, height,
- x: docked === 'right' ? cv.width - width + PEEK_HIDDEN : -PEEK_HIDDEN, y: cv.height - height - 2 };
+ x: docked === 'right' ? cv.width - width + hidden : -hidden, y: cv.height - height - 2 };
}
const width = w * frameScale, height = h * frameScale;
return { w, h, scale: frameScale, width, height, x: (cv.width - width) / 2, y: cv.height - height - 8 };
@@ -124,9 +135,10 @@
else if (kindOfMotion === 'wiggle') o.angle = Math.sin(now / 160) * 0.05;
else if (kindOfMotion === 'peek') {
// A shy, slow lean in and out; hovering or a waiting session makes her lean in further.
- const sway = (1 - Math.cos(now / 1900)) * 3;
+ // The sway spans two thirds of the hidden strip, so it never stalls against that limit.
+ const sway = (1 - Math.cos(now / 1900)) * peekStyle().hidden / 3;
peekLean += ((hovered ? 12 : base === 'waiting' ? 9 : 0) - peekLean) * 0.12;
- o.dx = (docked === 'right' ? -1 : 1) * Math.min(PEEK_HIDDEN, sway + peekLean);
+ o.dx = (docked === 'right' ? -1 : 1) * Math.min(peekStyle().hidden, sway + peekLean);
const since = now - peekBounceAt;
if (peekBounce === 'happy' && since < 900) o.dy = -Math.abs(Math.sin(since / 90)) * 6 * (1 - since / 900);
else if (peekBounce === 'shake' && since < 700) o.dx += Math.sin(since / 28) * 3 * (1 - since / 700);
@@ -142,7 +154,7 @@
target.clearRect(0, 0, cv.width, cv.height);
if (g.peek) {
target.save();
- target.imageSmoothingEnabled = true;
+ target.imageSmoothingEnabled = peekStyle().smooth;
if (o) target.translate(o.dx, o.dy);
// The art peeks from the right edge; mirror it for the left edge.
if (docked === 'left') { target.translate(g.x * 2 + g.width, 0); target.scale(-1, 1); }
@@ -166,7 +178,6 @@
function render() {
if (!sheet || !data) return;
const g = geometry();
- spriteRect = g;
paint(ctx, g, offsetAt(performance.now(), g));
if (!rafId && continuous()) rafId = requestAnimationFrame(() => { rafId = 0; render(); });
}
@@ -188,8 +199,11 @@
}
bandCache.set(key, bands);
}
- const [mx, my] = MOTION_MARGIN[activeMotion()] || [0, 0];
+ const motion = activeMotion();
+ const [mx, my] = motion === 'peek' ? [peekStyle().hidden + 4, 8] : MOTION_MARGIN[motion] || [0, 0];
bodyRegions = bands.map(([l, t, r, b]) => [Math.max(0, l - mx), Math.max(0, t - my), Math.min(cv.width, r + mx), Math.min(cv.height, b + my)]);
+ bodyBounds = bands.length ? [Math.min(...bands.map(b => b[0])), Math.min(...bands.map(b => b[1])),
+ Math.max(...bands.map(b => b[2])), Math.max(...bands.map(b => b[3]))] : null;
}
function setFrame(frame) {
if (!Array.isArray(frame?.images)) return; // frames without images keep the previous picture
@@ -201,15 +215,25 @@
function reportRegions() {
const r = cv.getBoundingClientRect();
const toWindow = b => [b[0] / cv.width, b[1] / cv.height, b[2] / cv.width, b[3] / cv.height];
- // While hovered the whole sprite box stays hit-testable (as Codex does), so a jump that changes
- // the silhouette under a resting pointer cannot flicker the hover on and off.
- const g = spriteRect;
- const bodies = hovered && g ? [toWindow([g.x - 4, g.y - 6, g.x + g.width + 4, g.y + g.height + 2])] : bodyRegions.map(toWindow);
- // The fixed-size native handle uses these same CSS coordinates on both hosts.
- bodies.push(normalized($('grip').getBoundingClientRect()));
+ const bodies = bodyRegions.map(toWindow);
+ // Keep only a small anchor under a stationary pointer when the animation jumps away.
+ // The rest of the transparent sprite frame remains click-through during hover.
+ if (hovered && hoverPoint) bodies.push(toWindow([hoverPoint.x - 3, hoverPoint.y - 3, hoverPoint.x + 3, hoverPoint.y + 3]));
+ const grip = $('grip');
+ grip.hidden = !bodyBounds;
+ // It follows the animated outline, but holds still under the pointer so it can be grabbed.
+ // It also stays below the collapsed card stack: that box (with its shadow padding) sits above
+ // the grip and would swallow the pointer, and macOS treats it as foreground, not the grip.
+ if (bodyBounds && !grip.matches(':hover')) {
+ const belowCards = stack.hidden || expanded ? 0 : stack.getBoundingClientRect().bottom;
+ const top = Math.max(0, belowCards, bodyBounds[1] * r.height / cv.height - grip.offsetHeight);
+ grip.style.left = Math.max(0, bodyBounds[0] * r.width / cv.width - grip.offsetWidth) + 'px';
+ grip.style.top = Math.min(Math.max(0, innerHeight - grip.offsetHeight), top) + 'px';
+ }
// No card, no rectangle: the empty space above the pet stays click-through.
const message = { type: 'overlay', controls: null,
- rect: stack.hidden ? null : normalized(stack.getBoundingClientRect()), bubbles: bodies };
+ rect: stack.hidden ? null : normalized(stack.getBoundingClientRect()),
+ grip: grip.hidden ? null : normalized(grip.getBoundingClientRect()), bubbles: bodies };
const key = JSON.stringify(message);
if (key !== overlayKey && r.width > 0) { overlayKey = key; action(message); }
}
@@ -263,7 +287,7 @@
animation = rest.name; flipped = false; motionKind = ''; frameIndex = 0;
setFrame(data.animations[rest.name].frames[0]);
}
- // ---- Edge peek (docked Whale Girl): blink now and then, happy squint on attention ----
+ // ---- Edge peek (docked pet): blink now and then, happy squint on attention ----
function setPeekEyes(value) {
peekEyes = value; frameIndex = value;
updateBands(); render(); reportRegions();
@@ -295,8 +319,9 @@
timer = setTimeout(() => { if (token === generation) playPeek(); }, kind === 'happy' ? 900 : 700);
}
function setDocked(side, { quiet = false } = {}) {
- const next_ = side === 'left' || side === 'right' ? side : '';
- if (next_ === docked || (next_ && !peek)) return;
+ windowEdge = side === 'left' || side === 'right' ? side : '';
+ const next_ = peek ? windowEdge : '';
+ if (next_ === docked) return;
docked = next_; peekLean = 0; peekBounce = '';
if (!sheet || quiet) return;
next();
@@ -415,7 +440,26 @@
stack.classList.toggle('layers-2', !expanded && cards.length > 2);
stack.setAttribute('aria-label', cards.length > 1 && !expanded ? `${cards.length} 个会话动态,点击展开` : '会话动态');
deck.replaceChildren(...cards.map(cardElement));
- reportRegions();
+ fitStack(); reportRegions();
+ }
+ // The cards keep the transcript font size, but the pet shrinks with the window: below roughly
+ // 280px the collapsed stack would run past the top edge and be cut off. Shrink it just enough
+ // to fit above the pet (not below 9px text), else pin it to the top edge. The spread-out list
+ // scrolls inside the window and keeps the collapsed size.
+ function fitStack() {
+ if (expanded) return;
+ stack.style.removeProperty('--stack-fit');
+ stack.classList.remove('pinned');
+ if (stack.hidden) return;
+ const minFit = Math.min(1, 9 / parseFloat(getComputedStyle(stack).fontSize));
+ let fit = 1;
+ for (let pass = 0; pass < 3; pass++) {
+ const box = stack.getBoundingClientRect();
+ if (box.top >= -0.5 || fit <= minFit) break;
+ fit = Math.max(minFit, fit * box.bottom / box.height);
+ stack.style.setProperty('--stack-fit', fit.toFixed(3));
+ }
+ if (stack.getBoundingClientRect().top < -0.5) stack.classList.add('pinned');
}
function setExpanded(value) {
clearTimeout(collapseTimer);
@@ -501,8 +545,10 @@
function applyTranscriptFont(font) {
const root = document.documentElement.style;
const size = String(font?.size || '').trim(), family = String(font?.family || '').trim();
+ const before = root.cssText;
if (/^\d+(\.\d+)?px$/.test(size)) root.setProperty('--pet-font-size', size);
if (family && !/[;{}<>]/.test(family)) root.setProperty('--pet-font', family);
+ if (root.cssText !== before) { fitStack(); reportRegions(); }
}
function applySnapshot(value) {
if (value?.version !== 1 || !Array.isArray(value.agents) || !Array.isArray(value.offices)) return;
@@ -528,14 +574,15 @@
try { await image.decode(); } catch {
if (revision === loadRevision) {
deck.replaceChildren(cardElement({ key: 'load-failed', status: 'failed', title: '宠物素材加载失败', body: '请关闭后重新开启', local: true }));
- stack.hidden = false; reportRegions();
+ stack.hidden = false; fitStack(); reportRegions();
}
return;
}
if (revision !== loadRevision) return;
kind = value.kind; sheet = image; data = window.ClassicPetAgents[kind]; profile = normalizeProfile(PROFILES[kind]);
peek = peekImage ? { image: peekImage, fw: peekImage.naturalWidth / 2, fh: peekImage.naturalHeight } : null;
- docked = '';
+ docked = peek ? windowEdge : ''; peekLean = 0; peekBounce = '';
+ hovered = false; hoverPoint = null;
bandCache = new Map(); images = null; base = 'idle'; variant = ''; queue = []; current = null;
stateKey = ''; sleeping = false; lastFidget = '';
// A newly shown pet greets instead of celebrating results that were already waiting.
@@ -597,16 +644,19 @@
const r = cv.getBoundingClientRect();
return { x: (event.clientX - r.left) * cv.width / r.width, y: (event.clientY - r.top) * cv.height / r.height };
}
- function overSprite(event) {
- const g = spriteRect;
- if (!g || !images?.length) return false;
+ function overSprite(event, updateAnchor = false) {
const p = canvasPoint(event);
- return p.x >= g.x - 4 && p.x <= g.x + g.width + 4 && p.y >= g.y - 6 && p.y <= g.y + g.height + 2;
+ if (bodyRegions.some(([l, t, r, b]) => p.x >= l && p.x < r && p.y >= t && p.y < b)) {
+ if (updateAnchor) hoverPoint = p;
+ return true;
+ }
+ return Boolean(hovered && hoverPoint && Math.abs(p.x - hoverPoint.x) <= 3 && Math.abs(p.y - hoverPoint.y) <= 3);
}
function setHovered(value) {
clearTimeout(hoverOffTimer);
if (hovered === value) return;
hovered = value;
+ if (!value) hoverPoint = null;
reportRegions();
if (!value || dragging) return;
const now = performance.now();
@@ -634,7 +684,9 @@
}
// The native move loop swallows the release; the next free move ends any stale drag pose.
if (dragging && !(event.buttons & 1)) endDrag();
- if (overSprite(event)) setHovered(true); else hoverOff();
+ if (overSprite(event, true)) {
+ setHovered(true); reportRegions();
+ } else hoverOff();
});
cv.addEventListener('pointerleave', hoverOff);
cv.addEventListener('pointerup', event => {
@@ -690,7 +742,7 @@
document.addEventListener('visibilitychange', () => {
if (document.hidden) { stopPlayback(); cancelAnimationFrame(rafId); rafId = 0; } else if (sheet) next();
});
- window.addEventListener('resize', reportRegions);
+ window.addEventListener('resize', () => { fitStack(); reportRegions(); });
window.addEventListener('pagehide', () => {
stopPlayback(); loadRevision++;
for (const id of [collapseTimer, badgeTimer, fidgetTimer, greetingTimer, dragPauseTimer, dragWatchTimer, hoverOffTimer]) clearTimeout(id);
@@ -709,7 +761,7 @@
const selected = new URLSearchParams(location.search).get('pet');
if (PROFILES[selected]) void setCharacter({ kind: selected,
sprite: selected === 'whale' ? './whale/map.png' : `./classic/${selected}/map.png`,
- peek: selected === 'whale' ? './whale/peek.png' : undefined });
+ peek: selected === 'whale' ? './whale/peek.png' : `./classic/${selected}/peek.png` });
}
reportRegions(); post('ready');
})();
diff --git a/assets/desktop_pet/whale/README.md b/assets/desktop_pet/whale/README.md
index aad5caebf..2b26da51d 100644
--- a/assets/desktop_pet/whale/README.md
+++ b/assets/desktop_pet/whale/README.md
@@ -8,4 +8,4 @@
## 趴边偷看(peek.png)
-鲸鱼娘拖到屏幕左/右边缘吸附后改用 `peek.png`:两帧并排(左睁眼、右闭眼),每帧 178×300,透明背景,画面右缘就是她躲着的那条边(贴左边时页面水平镜像)。不来自上游项目,2026-10-16 用 Codex 的内置图像生成(codex-cli 0.162,`image_generation` 功能)绘制,以本目录 idle / welcome / joy 的首帧作为风格参考,要求同一角色设计与画风:从画面右缘探出半个头、双手扒着边、好奇又害羞地偷看。闭眼帧由同一张图做「只闭眼、其余不变」的编辑,再只把眼睛区域贴回睁眼图,眨眼时其余线条不抖动。纯绿背景抠成透明并去绿边,在她扒着的那条边线处裁掉,两帧同裁同缩放。这张原画比精灵帧大约 1.8 倍,页面按常规帧缩放 ÷ 1.8 绘制(`classic_pet.js` 的 `PEEK_ART_RATIO`),换图时要重新量这个比例。生成用的是 ZipZipPipe 设计的角色形象,沿用上游许可说明。
+鲸鱼娘拖到屏幕左/右边缘吸附后改用 `peek.png`:两帧并排(左睁眼、右闭眼),每帧 178×300,透明背景,画面右缘就是她躲着的那条边(贴左边时页面水平镜像)。不来自上游项目,2026-10-16 用 Codex 的内置图像生成(codex-cli 0.162,`image_generation` 功能)绘制,以本目录 idle / welcome / joy 的首帧作为风格参考,要求同一角色设计与画风:从画面右缘探出半个头、双手扒着边、好奇又害羞地偷看。闭眼帧由同一张图做「只闭眼、其余不变」的编辑,再只把眼睛区域贴回睁眼图,眨眼时其余线条不抖动。纯绿背景抠成透明并去绿边,在她扒着的那条边线处裁掉,两帧同裁同缩放。这张原画比精灵帧大约 1.8 倍,页面按常规帧缩放 ÷ 1.8 绘制(`classic_pet.js` 的 `PEEK.whale.ratio`),换图时要重新量这个比例。生成用的是 ZipZipPipe 设计的角色形象,沿用上游许可说明。
diff --git a/assets/seed/MANIFEST.json b/assets/seed/MANIFEST.json
index 4b0beb51d..2ac2c1793 100644
--- a/assets/seed/MANIFEST.json
+++ b/assets/seed/MANIFEST.json
@@ -1,5 +1,5 @@
{
- "bundle_version": "2026-10-15.1",
+ "bundle_version": "2026-10-16.1",
"description": "Versioned default ACECode global Skill, expert, and managed hook seed bundle reconciled at startup.",
"skills": [
{
@@ -24,7 +24,7 @@
"name": "expert-manager",
"source_id": "acecode:expert-manager@2026-08-09",
"relative_path": "expert-management/expert-manager",
- "skill_md_sha256": "804fbfefb9cfb49a047182b82f1f6b6f42eb3ba1f42abb87e412fcaf24ef20a8"
+ "skill_md_sha256": "54250804b66e1e5b8658f7c56a3e4cd1fcd175cdfe2a583e1b2b9017ab0c0dc9"
},
{
"name": "native-mcp",
diff --git a/assets/seed/seed.version b/assets/seed/seed.version
index 2e7a6e1d0..ed433c0e1 100644
--- a/assets/seed/seed.version
+++ b/assets/seed/seed.version
@@ -1 +1 @@
-2026-10-15.1
+2026-10-16.1
diff --git a/assets/seed/skills/expert-management/expert-manager/SKILL.md b/assets/seed/skills/expert-management/expert-manager/SKILL.md
index 6597412bb..119b9968e 100644
--- a/assets/seed/skills/expert-management/expert-manager/SKILL.md
+++ b/assets/seed/skills/expert-management/expert-manager/SKILL.md
@@ -34,6 +34,9 @@ description: 创建、导入、修改、校验和打包 ACECode 专家组件;
- 如果已有资料足够,直接转化,不重复询问资料里已经明确的信息。
- 高级能力默认保持“继承全局”。只有用户明确要求限制或启用特定 Skill、MCP
服务器、本地工具时,才写入 `expert.json.capabilities`。
+- 采样参数默认继承模型。用户指定时写入 `expert.json.sampling`,支持
+ `temperature`、`top_p`、`top_k`、`min_p`;批量定义也接受同名对象。
+ 不把采样值写成 Agent 指令或未经解析的 frontmatter。具体范围见包格式文档。
## 开始前检查
diff --git a/assets/seed/skills/expert-management/expert-manager/references/expert-json-spec.md b/assets/seed/skills/expert-management/expert-manager/references/expert-json-spec.md
index 2b191b4af..1ee5d50fa 100644
--- a/assets/seed/skills/expert-management/expert-manager/references/expert-json-spec.md
+++ b/assets/seed/skills/expert-management/expert-manager/references/expert-json-spec.md
@@ -17,6 +17,27 @@
- 不写 `author`。ACECode 只使用 `displayName` 作为专家对外名称;来源和许可
信息保留在 `NOTICE.md`、`license` 与 `homepage` 中。
+## 采样参数(可选)
+
+Agent 和 Team 的 `expert.json` 都可包含:
+
+```json
+"sampling": { "temperature": 0.3, "top_p": 0.95, "top_k": 20, "min_p": 0 }
+```
+
+每个字段独立可选,`null` 表示继承:temperature 为 0 到 2,top_p 大于 0 且不超过 1,
+top_k 为 1 到 2147483647 的整数,min_p 为 0 到 1。数值 0 必须保留。
+包内 `agents` 对象项也可声明 `sampling`,覆盖包级默认值。
+团队引用专家时使用各专家的有效预设,团队默认值补充未设置字段。
+子 agent 发起时传入的 `sampling` 具有最高优先级,模型设置作为最终默认值。
+
+实际运行仍受所选 provider/protocol 限制。Responses 不支持 top_k/min_p,Anthropic
+不支持 min_p 且 temperature 不超过 1。无法生效的设置会在会话启动或切换时被拒绝。
+管理 API 更新时省略 sampling 保留原预设,传 null 或空对象清除全部预设。
+单专家详情返回包含 lead 覆盖后的有效值;显式更新 sampling 时一并清除 lead 的旧覆盖。
+专家团的更新只替换团队默认值,被引用专家的预设保持独立。
+`batch_create.py` 接受同名字段,`validate_expert.py` 校验类型、范围和未知键。
+
## Agent 型必需字段
```json
diff --git a/assets/seed/skills/expert-management/expert-manager/scripts/batch_create.py b/assets/seed/skills/expert-management/expert-manager/scripts/batch_create.py
index 1fb1231a0..d99bf6374 100644
--- a/assets/seed/skills/expert-management/expert-manager/scripts/batch_create.py
+++ b/assets/seed/skills/expert-management/expert-manager/scripts/batch_create.py
@@ -42,6 +42,7 @@
"homepage",
"avatar",
"stateAvatars",
+ "sampling",
}
AGENT_INPUT_KEYS = COMMON_INPUT_KEYS | {"instructions", "capabilities"}
TEAM_INPUT_KEYS = COMMON_INPUT_KEYS | {"leadExpert", "memberExperts"}
@@ -67,7 +68,7 @@ def quick_prompts(item: dict[str, Any]) -> list[str]:
def copy_optional_metadata(item: dict[str, Any], manifest: dict[str, Any]) -> None:
- for key in ("license", "homepage"):
+ for key in ("license", "homepage", "sampling"):
if key in item:
manifest[key] = item[key]
diff --git a/assets/seed/skills/expert-management/expert-manager/scripts/validate_expert.py b/assets/seed/skills/expert-management/expert-manager/scripts/validate_expert.py
index af589bf4b..4f622ab1e 100644
--- a/assets/seed/skills/expert-management/expert-manager/scripts/validate_expert.py
+++ b/assets/seed/skills/expert-management/expert-manager/scripts/validate_expert.py
@@ -13,6 +13,7 @@
import argparse
import json
+import math
import os
import re
from pathlib import Path
@@ -323,6 +324,32 @@ def validate_capabilities(
seen.add(normalized)
+def validate_sampling(value: Any, result: ValidationResult, label: str = "sampling") -> None:
+ if value is None:
+ return
+ if not isinstance(value, dict):
+ result.error(f"{label} must be an object or null")
+ return
+ fields = {"temperature", "top_p", "top_k", "min_p"}
+ for key, number in value.items():
+ if key not in fields:
+ result.error(f"{label}: unknown field {key}")
+ continue
+ if number is None:
+ continue
+ if type(number) not in (int, float) or (type(number) is float and not math.isfinite(number)):
+ result.error(f"{label}.{key} must be a finite number")
+ continue
+ valid = {
+ "temperature": 0 <= number <= 2,
+ "top_p": 0 < number <= 1,
+ "top_k": type(number) is int and 1 <= number <= 2147483647,
+ "min_p": 0 <= number <= 1,
+ }[key]
+ if not valid:
+ result.error(f"{label}.{key} is outside its supported range")
+
+
def validate_agent(
package_root: Path, manifest: dict[str, Any], result: ValidationResult
) -> None:
@@ -340,6 +367,7 @@ def validate_agent(
elif isinstance(entry, dict):
agent_id = localized_text(entry.get("id"))
raw_path = localized_text(entry.get("path"))
+ validate_sampling(entry.get("sampling"), result, f"agents[{index}].sampling")
else:
result.error(f"expert.json: agents[{index}] must be a path or object")
continue
@@ -485,6 +513,7 @@ def validate_expert(
validate_display_fields(manifest, result)
validate_avatar_and_skills(package_root, manifest, result)
validate_capabilities(manifest, expert_type, result)
+ validate_sampling(manifest.get("sampling"), result)
if expert_type == "agent":
validate_agent(package_root, manifest, result)
diff --git a/cmake/acecode_desktop.cmake b/cmake/acecode_desktop.cmake
index 6cbf8f24a..f7dfc8cbd 100644
--- a/cmake/acecode_desktop.cmake
+++ b/cmake/acecode_desktop.cmake
@@ -177,8 +177,10 @@ if(WIN32 OR APPLE)
${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic_pet.js
${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/clippy/agent.js
${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/clippy/map.png
+ ${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/clippy/peek.png
${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/rover/agent.js
${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/rover/map.png
+ ${CMAKE_SOURCE_DIR}/assets/desktop_pet/classic/rover/peek.png
${CMAKE_SOURCE_DIR}/assets/desktop_pet/whale/agent.js
${CMAKE_SOURCE_DIR}/assets/desktop_pet/whale/map.png
${CMAKE_SOURCE_DIR}/assets/desktop_pet/whale/peek.png
diff --git a/docs/agent-browser-performance.md b/docs/agent-browser-performance.md
new file mode 100644
index 000000000..04f372793
--- /dev/null
+++ b/docs/agent-browser-performance.md
@@ -0,0 +1,328 @@
+# Agent Browser 性能改造方案
+
+对标项目:[browser-use/jev-ultrafast](https://github.com/browser-use/jev-ultrafast)(2026-09 抓取)。
+
+本文只谈**延迟**。正确性与隐私类问题另见文末「附录 B」,不在本方案范围内。
+
+---
+
+## 一、背景:jev 的「7 秒」是怎么来的
+
+jev-ultrafast 的卖点是一句话:一条自然语言目标,Google Flights 上从苏黎世搜到伦敦,
+**7.1 秒**,含模型调用、文本生成、浏览器操作和加载等待。
+
+拆开看,这 7 秒的构成与直觉相反:
+
+| 来源 | 占比 | 说明 |
+| --- | --- | --- |
+| 模型时间 | 绝对主导 | 每步一次模型请求,约 10 余步 |
+| 浏览器协议时间 | 零点几秒 | 整个任务 101 次 CDP 调用,持久会话上每次约毫秒级 |
+
+它自己公布的优化前后对比更说明问题:
+
+| 指标 | 优化前 | 优化后 | 变化 |
+| --- | --- | --- | --- |
+| 浏览器协议调用 | 1092 | 101 | 降低 91% |
+| 任务耗时中位数 | 9.450 秒 | 7.092 秒 | 降低 25% |
+
+**把浏览器协议调用砍掉 91%,任务只快了 25%。** 这是本方案最重要的一条背景事实:
+jev 的速度主要不来自浏览器代码,而来自它的**模型形态**和**每步只发一次请求**。
+
+它快的三个真实原因:
+
+1. **模型不是通用 LLM。** 决策模型是 TypeSafe 的 Jev,一个约束解码的分类器:输出一个
+ 操作枚举(`CLICK` / `TYPE_TEXT` / `SELECT` / `SCROLL_UP` / `SCROLL_DOWN` / `WAIT` /
+ `DONE` / `BLOCKED`)加一个元素序号。只有需要输入文本时才调一次小而快的文本模型,
+ 当前 demo 用 `inception/mercury-2.5` 且关闭 reasoning。
+2. **每个决策周期一次网络往返。** 操作头和目标头共享同一份观测状态,投机式扇出,
+ 一次请求同时拿到「做什么」和「对谁做」。目标头里只包含与该操作兼容的元素。
+3. **步数少,且每步的状态是替换而非累积。** 模型每步只看当前元素表,历史是一份
+ 紧凑的决策列表,不是一堆过期页面全文。
+
+它明确不做的:默认循环里**没有截图**;结构化状态就是全部输入。截图只给它的可视化
+inspector 用,演示视频走独立的 screencast。
+
+### jev 在浏览器侧做的事(值得借的部分)
+
+即使浏览器侧只值 25%,这 25% 的做法仍然干净,值得参考:
+
+- **一次快照一次调用。** `jev_ultrafast/snapshot.js` 一个 `Runtime.evaluate` 原子返回
+ url、title、viewport、可见文本、动作表、新鲜度标记、逐节点守卫。
+- **只发可见内容。** 元素中心点落在视口外的直接丢弃;正文用 Range 的 rect 判断是否
+ 真在屏幕上,上限 6000 字符。屏幕外的正文和页脚不进模型上下文。
+- **动作空间而非元素空间。** 表里每一行都是可执行动作:`