diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8475c5..96c9788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,3 +41,6 @@ jobs: - name: Lint Swift formatting run: xcrun swift-format lint --strict --recursive Sources Tests Package.swift + + - name: Package hardened app bundle + run: scripts/package_app.sh diff --git a/.gitignore b/.gitignore index a77e8cd..0ec8bad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .build/ .swiftpm/ .DS_Store +dist/ *.xcodeproj/ *.xcworkspace/ DerivedData/ diff --git a/README.md b/README.md index a735b0e..d729f64 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,21 @@ This is the desktop component of the work tracked in swift build swift run agentd # foreground; menu-bar item appears swift test +scripts/package_app.sh # release .app bundle with hardened runtime signing ``` First run will trigger the system Screen Recording and Accessibility prompts the first time the gated APIs are called. Grant both in System Settings → Privacy & Security and relaunch. +`scripts/package_app.sh` creates `dist/EvalOps agentd.app` and +`dist/agentd.zip`. By default CI uses ad-hoc signing with hardened runtime so +the bundle shape and entitlements are continuously checked. For release signing, +set `AGENTD_CODESIGN_IDENTITY` to a Developer ID Application identity. To +notarize and staple the bundle, either set `AGENTD_NOTARY_PROFILE` for a +notarytool keychain profile or set `AGENTD_NOTARY_APPLE_ID`, +`AGENTD_NOTARY_TEAM_ID`, and `AGENTD_NOTARY_PASSWORD`. + ## Configuration agentd reads and writes `~/.evalops/agentd/config.json`. Important defaults: @@ -116,7 +125,8 @@ locally and does not write the broker session token to disk. - Calendar / Zoom auto-pause via NATS subject `chronicle.policy.pause` (siphon-fed). - Encryption-at-rest option for local batches. -- Notarized + hardened-runtime signed `.app` bundle. +- Hardware-backed permission-flow smoke test for Screen Recording and + Accessibility prompts. ## Layout diff --git a/scripts/package_app.sh b/scripts/package_app.sh new file mode 100755 index 0000000..7bc0e55 --- /dev/null +++ b/scripts/package_app.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +product="${AGENTD_PRODUCT:-agentd}" +configuration="${CONFIGURATION:-release}" +dist_dir="${DIST_DIR:-"$root/dist"}" +app_name="${AGENTD_APP_NAME:-EvalOps agentd}" +app_path="$dist_dir/$app_name.app" +zip_path="$dist_dir/$product.zip" +identity="${AGENTD_CODESIGN_IDENTITY:--}" +entitlements="${AGENTD_ENTITLEMENTS:-"$root/support/agentd.entitlements"}" + +mkdir -p "$dist_dir" + +swift build -c "$configuration" --product "$product" +build_bin_dir="$(swift build -c "$configuration" --product "$product" --show-bin-path)" +binary="$build_bin_dir/$product" + +rm -rf "$app_path" "$zip_path" +mkdir -p "$app_path/Contents/MacOS" "$app_path/Contents/Resources" +cp "$root/support/Info.plist" "$app_path/Contents/Info.plist" +cp "$binary" "$app_path/Contents/MacOS/$product" +chmod 0755 "$app_path/Contents/MacOS/$product" + +codesign_args=( + --force + --sign "$identity" + --options runtime + --entitlements "$entitlements" +) +if [[ "$identity" != "-" ]]; then + codesign_args+=(--timestamp) +fi +codesign "${codesign_args[@]}" "$app_path" +codesign --verify --strict --deep --verbose=2 "$app_path" + +ditto -c -k --keepParent "$app_path" "$zip_path" + +notarized=0 +if [[ -n "${AGENTD_NOTARY_PROFILE:-}" ]]; then + xcrun notarytool submit "$zip_path" --keychain-profile "$AGENTD_NOTARY_PROFILE" --wait + xcrun stapler staple "$app_path" + notarized=1 +elif [[ -n "${AGENTD_NOTARY_APPLE_ID:-}" && -n "${AGENTD_NOTARY_TEAM_ID:-}" && -n "${AGENTD_NOTARY_PASSWORD:-}" ]]; then + xcrun notarytool submit "$zip_path" \ + --apple-id "$AGENTD_NOTARY_APPLE_ID" \ + --team-id "$AGENTD_NOTARY_TEAM_ID" \ + --password "$AGENTD_NOTARY_PASSWORD" \ + --wait + xcrun stapler staple "$app_path" + notarized=1 +else + echo "Skipping notarization: set AGENTD_NOTARY_PROFILE or AGENTD_NOTARY_APPLE_ID/TEAM_ID/PASSWORD." +fi + +if [[ "$notarized" == "1" ]] && command -v spctl >/dev/null 2>&1; then + spctl -a -t exec -vv "$app_path" +fi + +echo "Packaged $app_path" +echo "Archive $zip_path" diff --git a/support/agentd.entitlements b/support/agentd.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/support/agentd.entitlements @@ -0,0 +1,5 @@ + + + + +