Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or requestready-for-agentFully specified; an agent may claim itFully specified; an agent may claim itrisk:securityTouches broker, approval, delivery, crypto or keychain; requires line-by-line reviewTouches broker, approval, delivery, crypto or keychain; requires line-by-line review
Description
Goal
An Ask Key installed at /Applications/Ask Key.app checks a signed appcast and, after the user confirms, installs a notarized update with Sparkle 2; Debug, Local and E2E builds never run the updater.
Context
docs/adr/0006-release-process.md(Publication; Not included) anddocs/adr/0005-distribution-and-client-integration.md. AGENTS.md says not to add Sparkle unless an issue asks for it: this issue asks for it and approves Sparkle as a new third-party dependency.docs/features.md, row "Sparkle and in-app update checks" (currently Remove). The removed Lokalite-eraSoftwareUpdateris insudoHG/AskKey-legacyatlegacy-final; read it only as a reference (see issue-workflow.md → Working in the legacy code).scripts/build-app.sh: Info.plist generation, theLocalmode comment ("It does not enable software updates"), and signing withcodesign --force --deep.AskKeyApp.entitlements: the app is not sandboxed and must stay that way.Sources/AskKeyApp/AskKeyAppEntry.swift(production entry),Sources/AskKeyAppKit/App/AppDelegate+StatusItem.swift(right-click menu),Sources/AskKeyAppKit/Views/Settings/FrozenSettingsPage.swift(General section),Sources/AskKeyBroker/OfficialInstallTopology.swift.- Sparkle documentation: https://sparkle-project.org/documentation/, Customization, Publishing.
Decisions (record them in a new ADR 0011)
- Dependency. Sparkle 2 through SwiftPM, pinned with
exact:to a 2.x release that supports signed feeds (2.9 or later). Only the productionAskKeyAppexecutable target depends on it.AskKeyAppKitdefines the updater interface and the pure policy types, soswift test, the E2E app and theaskkeyhelper never link Sparkle. - Feed.
SUFeedURLishttps://askkey.sudohg.com/appcast.xml, on the maintainer's own domain. The website answers it with a temporary (302) redirect tohttps://github.com/sudoHG/AskKey/releases/latest/download/appcast.xml(set up in Publish a signed appcast with each release #259). The feed is therefore a GitHub Release asset: it goes live when a release is published, which already needs the maintainer's approval, and releases need no website deploy. Every installed copy keeps requesting the URL on our domain, so treat it as a product identifier and add it to AGENTS.md → Product identifiers. Because the domain is ours, a later change can serve the feed directly or from a mirror for users in mainland China without a new release. Enclosures use versioned GitHub Release URLs (https://github.com/sudoHG/AskKey/releases/download/v<version>/AskKey-<version>.dmg), and a later appcast can point them elsewhere. Feeds and archives must be EdDSA-signed, so neither host is trusted for integrity:SUVerifyUpdateBeforeExtractionandSURequireSignedFeedareYES.SUSignedFeedFailureExpirationIntervalis0, so a feed that fails validation is never accepted later (Sparkle otherwise falls back after 20 days; added in review of Add an in-app updater with Sparkle 2 #260).SUPublicEDKeycomes from the committed filescripts/sparkle-public-key.txt(added by Create the Sparkle update-signing key #258). Until that file lands,Releasebuilds fail closed; this is intended, so no release ships an updater without a key. - No silent installs.
SUAllowsAutomaticUpdatesandSUAutomaticallyUpdateareNO, so every install needs the user's click.SUEnableAutomaticChecksstays unset, so Sparkle asks on the second launch whether to check automatically; Settings can change it later.SUEnableSystemProfilingisNO. Keep the default check interval (one day). Scheduled updates use Sparkle's gentle reminders: Sparkle still shows the alert without stealing focus, and, when notifications are already allowed, Ask Key also posts one notification that opens the update (added in review of Add an in-app updater with Sparkle 2 #260). - Where it runs. An updater is created only when all of these hold: the Info.plist has
SUFeedURL, which onlyReleasemode writes; the bundle ID iscom.sudohg.askkey.app; none ofAskKeyLocalTesting,AskKeyE2ETestingorAskKeyRequiresDebugRunDirectoryis set; andOfficialInstallTopologyallows the official runtime. Otherwise no Sparkle object exists and no update controls are shown. - Relaunch only when idle. Implement
updater:shouldPostponeRelaunchForUpdate:untilInvokingBlock:to postpone while an approval prompt is pending, a delivery, run or write session is in progress, or management authentication is active, and invoke the handler once the app is idle. An update never cancels, denies or orphans a pending request. Timed allowances do not postpone it, because they can last hours. Instead the Settings row states what a relaunch ends: Ask Key locks again and timed allowances held by the running broker end. Verify that wording against current behaviour. - Running agents. The install path, helper path and broker protocol version stay unchanged, so client configurations need no change, and helper processes that are already running keep working against the new broker (helper trust compares Team IDs). Tools added in a new version appear only after the agent restarts its MCP server. Say so in the ADR and the README.
- Downgrades and migrations. Sparkle only offers newer versions, and pulling a bad update means shipping a fixed, higher version. If automatic installs are ever enabled, every release that adds a library migration must set
sparkle:minimumAutoupdateVersionto its own version. - Network and privacy. The update check is Ask Key's only outbound network request. Record exactly what a check sends: the feed URL and Sparkle's request headers, including its User-Agent, confirmed from the pinned Sparkle source rather than by launching the app. A check goes to
askkey.sudohg.comthrough Cloudflare and follows the redirect to GitHub, and an update download goes to GitHub; both see the IP address. Document it inREADME.md,README.zh-CN.mdandSECURITY.md. - Known limitation. Sparkle's standard windows follow the macOS language, not Ask Key's in-app language setting. Accept this for now and note it in the ADR.
Scope
May change:
Package.swift,Package.resolved: add Sparkle onlySources/AskKeyApp/: the Sparkle adapter (SPUStandardUpdaterControllerandSPUUpdaterDelegate)Sources/AskKeyAppKit/: the updater interface, the enable and relaunch policy types, the Settings General rows, the status item menu, andLocalizable.xcstrings(English and Simplified Chinese)scripts/build-app.sh;scripts/check_module_deps.pyandscripts/check_release_symbols.shonly if they must accept the new dependency edgeTests/AskKeyAppTests/,Tests/Automation/AGENTS.md: only add the feed URL to Product identifiersdocs/adr/0011-in-app-updates.md(new); indocs/adr/0005-distribution-and-client-integration.mdanddocs/adr/0006-release-process.md, only the sentences that 0011 supersedes;docs/features.md;docs/architecture.md;README.md;README.zh-CN.md;SECURITY.md;CHANGELOG.md(Unreleased)
Must not touch:
- The broker protocol and the helper:
Sources/AskKeyHelper/, andSources/AskKeyBroker/beyond reading existing APIs Sources/AskKeyVault/, keychain code and every product identifierscripts/package-release.sh,scripts/notarize-release.py(that work is Publish a signed appcast with each release #259)AskKeyApp.entitlements,.github/workflows/,site/,site-src/scripts/sparkle-public-key.txt(added by Create the Sparkle update-signing key #258)
Steps
- Write ADR 0011 with the decisions above. Mark the 0006 sentences "There is no updater" and "Sparkle or any in-app updater … need their own issue" as superseded by 0011, and change the features.md row to Keep, citing 0011.
- Add Sparkle to
Package.swiftas a dependency ofAskKeyApponly. - In
AskKeyAppKit, add two pure types: one decides whether the updater is enabled (from the Info.plist values and the topology decision), the other decides whether a relaunch must wait (from the busy states in decision 5). Add a small protocol the UI uses for "check now", the automatic-check setting and availability. - In
AskKeyApp, implement that protocol with Sparkle. Create it only when the policy enables it, and wire the delegate's postpone method to the relaunch policy. - UI, shown only when the updater is enabled:
- Settings → General gets a "Check for Updates Automatically" switch, a "Check for Updates…" button, and a subtitle explaining what a relaunch ends.
- The status item's right-click menu gets "Check for Updates…" above "Quit Ask Key".
- Simplified Chinese strings use the catalog's existing terms (凭证, Agent).
scripts/build-app.sh, in order:- Copy
Sparkle.frameworkintoContents/Frameworksin every mode, so the binary always launches. - Remove the framework's
XPCServices(unneeded outside the sandbox) and make sure the executable's rpath includes@executable_path/../Frameworks. - Sign nested code inside out with the existing options (Sparkle's
Autoupdate,Updater.app, the framework, the helper, then the app), without--deepwhen signing. Keep--deepfor verification. - In
Releasemode only, write the Info.plist keys from decisions 2 and 3. Fail ifscripts/sparkle-public-key.txtis missing or is not a base64 32-byte key. - Debug,
Localand E2E modes write no Sparkle keys.
- Copy
- Tests:
- The enable policy covers every flag, the bundle-ID and topology case.
- The relaunch policy covers each busy state, idle, and a request that arrives while postponed.
- Automation tests check the Debug
build-app.shoutput: no Sparkle keys, the framework embedded and no XPC services.
- Update the docs in Scope.
Acceptance
swift build # succeeds
swift test --filter 'SoftwareUpdate' # new suites run; 0 failed (adjust the filter to the suite names you add and list them in the receipt)
python3 -m unittest discover -s Tests/Automation -v # 0 failed
python3 scripts/check_hygiene.py # passes
python3 scripts/check_module_deps.py # passes
bash scripts/check_release_symbols.sh # passes
git grep -n 'import Sparkle' -- Sources | grep -v '^Sources/AskKeyApp/' # no output
bash scripts/build-app.sh Debug # succeeds (needs 80 GiB free; do not launch the app)
/usr/libexec/PlistBuddy -c 'Print :SUFeedURL' .build/AskKeyApp.app/Contents/Info.plist # fails: key absent
test -d .build/AskKeyApp.app/Contents/Frameworks/Sparkle.framework # succeeds
find .build/AskKeyApp.app -name '*.xpc' | wc -l # 0
codesign --verify --strict --deep .build/AskKeyApp.app # succeeds
otool -l .build/AskKeyApp.app/Contents/MacOS/AskKeyApp | grep -A2 LC_RPATH | grep -c '@executable_path/../Frameworks' # 1 or moreBoth CI jobs are green, including test-dev-launch.sh (the Debug app still launches) and basic-ui-flows. The receipt lists the headers from decision 8 and the Sparkle source lines they come from.
Before the merge, the planner renders Settings → General and the status menu in English and Simplified Chinese offscreen. This is not an executor step.
Blocked by
None
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestready-for-agentFully specified; an agent may claim itFully specified; an agent may claim itrisk:securityTouches broker, approval, delivery, crypto or keychain; requires line-by-line reviewTouches broker, approval, delivery, crypto or keychain; requires line-by-line review
Activity
Address review round 1 on the Sparkle updater (#257)
Ship Sparkle.framework in the packaged authentication test app (#257)