Mac App Store 不讓視窗管理器存在,我還是把它上架了
一位 Apple 開發者技術支援(DTS)工程師告訴我,我的這個 App 不可能做得出來:「無法在沙箱化的 App 中使用 Accessibility API……您唯一的出路是直接發行您的 App。」1 那個 App 是一款視窗排列工具,而 Accessibility API 正是每一款視窗管理器賴以存在的基礎,Mac App Store 上的每一個 App 都執行在禁用它的沙箱之內。可是上週,941 Tiles 還是在 Mac App Store 上線了:在選單列點一下,某個 App 散落各處的視窗就會全部對齊成一個均勻的網格。沙箱規則一次也沒有被折彎。關鍵在於注意到一件事:macOS 裡本來就有一個獲得 Apple 完全認可、握有特權的視窗搬運者,而且它接受 JSON。
TL;DR
- App Sandbox 一律阻擋跨 App 的 Accessibility 呼叫,沒有任何 entitlement 能解開它,而 Mac App Store 上的每一個 App 都必須沙箱化。自 2012 年 6 月起,視窗管理器這個類別對新進者實際上已經關閉;至今仍留在商店裡的排列工具都早於這項強制規定,因此可以永遠不沙箱化地執行。134
- 那扇獲准的門:沙箱化的 App 可以透過 Apple events 驅動 Apple 的 Shortcuts 引擎,而 Shortcuts 內建 Find Windows / Move Window / Resize Window 動作,能以 Apple 自己的權限搬動其他 App 的視窗。25 我的 App 只負責計算每個視窗該去哪裡;真正搬動的是一個隨附的捷徑。這是委派,不是許可。
- 讓它成真的工程細節:以程式產生隨附的
.shortcut,做成WFWorkflowActions的二進位 plist;一份以視窗 z 順序為鍵的 JSON 框架酬載約定;針對幽靈視窗與索引超出範圍的防護欄;以及一套雙引擎的程式碼庫——Developer ID 版本走直接的 Accessibility,商店版本走 Shortcuts 委派。 - 在那台曾讓五個商店版本失敗的機器上,第一次成功執行:四個散落的終端機視窗在 0.6 秒內落到精確的四分區框內。
- 功勞歸於應得之人:這個機制已由 snApp(開放原始碼,2023 年)與 Align(2024 年)在實務上驗證。把「不可能」變成實作計畫的,正是讀了 snApp 的原始碼。2
那堵牆
941 Tiles 的第一個版本是一款典型的選單列排列工具。macOS 有一套 Accessibility 系統,在取得使用者許可後,可以讓一個 App 列舉並搬動另一個 App 的視窗(對視窗的位置與尺寸屬性呼叫 AXUIElementSetAttributeValue)。建立在這之上的 App,在數週的開發中運作得毫無瑕疵。我把整個 App Store Connect 的產品頁都備妥了:17 種語言的中繼資料、螢幕截圖、定價、21 個已上傳的版本。
接著我把一個 TestFlight 版本裝到第二台 Mac 上。Accessibility 權限已授予,App 也回報存取已開啟,可是它能讀到的視窗是 0 個。
這不是 bug。Mac App Store 上的每一個 App 都執行在 Apple 的 App Sandbox 裡,這是一條隔離規則:App 只能碰屬於自己的東西,正是為了讓它無法窺探或干擾你 Mac 上的其他部分。4 伸手去碰另一個 App 的視窗,恰恰是沙箱存在的目的所要阻止的。開發版本不沙箱化執行,所以數週以來一切順利;商店版本執行的,是我一直忽略的那條規則。沒有權限對話框、沒有 entitlement、沒有任何設定能解開它。當我直接去問 Apple 時,DTS 的回覆就是本文開頭那一段:到商店之外去發行。1
一個自 2012 年起關閉的類別
這堵牆之所以耐人尋味,在於站在牆另一側的是誰。Magnet、Moom、BetterSnapTool:今天你仍能在 Mac App Store 上買到的視窗工具。它們的產品頁早於 2012 年 6 月的沙箱強制規定,因此依據一條不會轉移給任何其他人的祖父條款,可以不沙箱化地執行。2012 年之後誕生的每一款視窗管理器(Rectangle、Swish、Loop、BetterTouchTool、Amethyst)都在商店之外發行。Apple 也曾在反方向上執行這條界線:在商店待了將近 13 年的 PopClip,在審核告知其開發者不得於沙箱化的更新中加入新的 Accessibility 功能後,於 2024 年 3 月離開。3
我想把自己的看法說清楚:沙箱規則是合理的。一個能悄無聲息地讀取並搬動你螢幕上每一個視窗的 App,就是一個能監看你的 App,而「相信我」不是安全模型。商店的承諾是:你在那裡安裝的任何東西都做不到這件事。不公平之處很窄、很具體:既有業者保住了一種新進者無論取得多少使用者同意都無法贏得的能力。這個類別沒有被禁止,它被化石化了。
轉折
DTS 的判定有一個破綻:商店裡本來就有反例。Align(2024 年)與 snApp(2023 年)都在排列視窗,都活在 Mac App Store 上,也都誕生於 2012 年之後很久。snApp 是開放原始碼的,所以這套機制並不是祕密,它是一個程式碼儲存庫。2
沙箱化的 App 從不碰另一個 App 的視窗。它透過兩項宣告的 entitlement 去驅動 Shortcuts,也就是 Apple 自己的自動化 App:
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.shortcuts.events</string>
</array>
自 macOS 12 起,Shortcuts 提供了名為 Find Windows、Move Window 與 Resize Window 的動作。5 Shortcuts 被允許搬動視窗,因為它是 Apple 的。於是 App 算出每個視窗該去哪裡,把這份計畫交給使用者安裝的隨附捷徑,由 Apple 自己的引擎完成搬動。沙箱從未被違反,因為那個沙箱化的行程從未執行特權動作。這是委派,不是許可。
這個形狀裡有某種近乎建築學的意味:安全邊界仍然停在 Apple 畫下的位置,而能力則藉由系統上唯一正當持有它的那個行程,越過了邊界。
打造過程
知道機制和把它做出來上架,是兩個不同的星期。讓它真正達到正式產品水準的部分包括:
以程式產生捷徑。 使用者不該手工拼裝一個 12 個動作的工作流程。隨附的 .shortcut 檔案是一份 WFWorkflowActions 的二進位 plist,由 App 在建置時產生,並以 shortcuts sign -m anyone 簽署,因此安裝只需在 Shortcuts App 裡點一下。執行期的約定是一份 JSON 酬載,經由 ScriptingBridge 的 shortcut.run(withInput:) 傳入:
{"app": "Ghostty", "frames": [{"x": 0, "y": 38, "w": 1280, "h": 771}, ...]}
視窗的身分就是 z 順序。 捷徑的 Find Windows 動作由前而後為視窗編號,CGWindowList 列舉的順序也一樣。因此酬載中的第 i 個位置承載的就是視窗 i 的框架;再依位置排名分配格子,網格便重現了「視窗會停在原本所在之處附近」。沒有視窗 ID 跨越邊界,因為邊界另一側根本不存在視窗 ID。
防護欄。 一個以 Count 為上限的 Items in Range 箝制,可防止過期的酬載索引到超出目前視窗清單的範圍。一道最小尺寸過濾(120×80)排除了 CGWindowList 會回報、而舊的 Accessibility 角色檢查曾經隱藏掉的幽靈停放面板。還有一個讓我意外的事實:列出視窗根本不需要任何權限,CGWindowList 在沙箱中是合法的。一直以來被禁止的只有搬動。
一套程式碼庫,兩個引擎。 App 在啟動時透過 APP_SANDBOX_CONTAINER_ID 環境變數偵測沙箱。商店版本經由 Shortcuts 排列;Developer ID 版本與開發版本走直接的 Accessibility 路徑。相同的 UI、相同的運算,不同的搬運者。
沒有通知的監看模式。 選擇性啟用的自動重排功能通常會訂閱 Accessibility 的視窗通知,而這同樣被沙箱拿掉了。它以視窗伺服器輪詢的形式活了下來:做便宜的 CGWindowList 差異比對,一有變化就重排。
老實說說代價
使用者要付一筆一次性的設定稅:安裝隨附的捷徑(點一下),並同意一次自動化授權對話框(就是標準的「941 Tiles 想要控制 Shortcuts」提示,透過 InfoPlist.xcstrings 在地化為全部 17 種語言)。此後的體驗與直接呼叫 API 的排列工具完全相同:點一下網格圖像,點一下某個 App,視窗就定位。Shortcuts 的執行是看不見的,用時遠不到一秒。
我不會假裝這筆稅等於零;它確實比 Magnet 多出一個畫面。它同時也是活在沙箱之內所需付出的全部代價——另一條路則是請使用者為一個他們還沒有理由信任的公證下載去繞過 Gatekeeper。
回報
還是那台曾讓五個商店版本失敗的 Intel Mac:安裝、設定捷徑、授權,然後四個散落的 Ghostty 視窗在第一次執行時於 0.6 秒內落到精確的四分區框內。接著是國際化工作(String Catalogs、CLDR 複數規則,包含阿拉伯文全部六個類別)、在地化的產品頁,以及送審。經過一次審核退件與約 30 分鐘的重新審核,941 Tiles 已上線:一次性 4.99 美元,macOS 15 以上,17 種語言,零資料蒐集,不存取網路。
可以推廣的部分
這個模式比視窗排列本身更大。當一個平台把某項能力關在邊界之後,答案很少是去和邊界硬碰;而是清點有哪些第一方行程已經握有這項能力,並且對外提供了獲准的介面。在 macOS 上,這份清單愈來愈指向同一個地方:Shortcuts。它已經悄悄成為系統的特權自動化仲介者,而且按設計就可以從沙箱內部觸及。我的 Shortcuts 指南深入談了這一層,941 Tiles 案例研究則配著架構圖走過同樣的故事。
沙箱盡了它的職責。App 還是上架了。這兩句話都為真,而它們之間的那扇門,是 Apple 自己的。
常見問題
沙箱化的 Mac App 可以使用 Accessibility API 嗎?
不行。App Sandbox 一律阻擋跨 App 的 Accessibility 呼叫,沒有任何 entitlement、權限對話框或設定能解開它。Apple 自家 DTS 給我的回覆是:「無法在沙箱化的 App 中使用 Accessibility API……您唯一的出路是直接發行您的 App。」開發版本不沙箱化執行,這就是為什麼一款排列工具可以正常運作好幾週,然後在商店版本裡讀到 0 個視窗。14
視窗管理器要怎樣才能在 Mac App Store 上架?
靠委派那個特權動作。沙箱化的 App 可以透過 Apple events 驅動 Apple 的 Shortcuts 引擎,而 Shortcuts 自 macOS 12 起就提供了 Find Windows、Move Window 與 Resize Window 動作。我的 App 算出每個視窗該去哪裡,把這份計畫交給使用者安裝的隨附捷徑,由它以 Apple 自己的權限完成搬動。沙箱從未被違反,因為那個沙箱化的行程從未執行特權動作。25
既然視窗管理器不可能上架,為什麼 Magnet 和 Moom 在商店裡?
它們的產品頁早於 2012 年 6 月的沙箱強制規定,因此依據一條不會轉移給任何其他人的祖父條款,可以不沙箱化地執行。2012 年之後誕生的每一款視窗管理器,包括 Rectangle、Swish、Loop、BetterTouchTool 與 Amethyst,都在商店之外發行。PopClip 在審核告知其開發者不得於沙箱化的更新中加入新的 Accessibility 功能後,於 2024 年 3 月離開商店。這個類別沒有被禁止,它被化石化了。34
Shortcuts 委派讓使用者付出什麼代價?
一筆一次性的設定稅:點一下安裝隨附的捷徑,然後同意一次自動化授權對話框。此後的體驗與直接呼叫 API 的排列工具完全相同:點一下網格圖像,點一下某個 App,視窗就定位,而 Shortcuts 的執行是看不見的,用時遠不到一秒。它比 Magnet 多出一個畫面,而這就是活在沙箱之內所需付出的全部代價。
一套程式碼庫如何同時支援沙箱版本與直接版本?
App 在啟動時透過 APP_SANDBOX_CONTAINER_ID 環境變數偵測沙箱。商店版本經由 Shortcuts 排列;Developer ID 版本與開發版本走直接的 Accessibility 路徑。相同的 UI、相同的運算,不同的搬運者。監看模式同樣會調整:沙箱拿掉了 Accessibility 的視窗通知,所以選擇性啟用的自動重排以便宜的 CGWindowList 輪詢存活下來,做差異比對,一有變化就重排。
參考資料
-
Apple Developer Forums,討論串 805556(2025 年 10 月),Apple DTS 的回覆:「無法在沙箱化的 App 中使用 Accessibility API……您唯一的出路是使用 Developer ID 簽署直接發行您的 App。」 ↩↩↩↩
-
Om Chachad,GitHub 上的 snApp(2023 年),這款開放原始碼的 Mac App Store 視窗排列工具,其原始碼記錄了 Shortcuts 委派機制:
com.apple.security.automation.apple-events指令碼目標 entitlement,加上一個以 Apple 的視窗動作組成的隨附捷徑。 ↩↩↩↩ -
Michael Tsai,〈PopClip Leaving the Mac App Store〉(2024 年 3 月 21 日),談的是 Apple 審核要求一款在商店待了將近 13 年的 App,從沙箱化的更新中移除新的 Accessibility 功能。 ↩↩↩
-
Apple,App Sandbox 文件,自 2012 年 6 月起 Mac App Store 上所有 App 都被要求採用的隔離模型。 ↩↩↩↩
-
Apple,Mac 版 Shortcuts 使用手冊,其中記載了自 macOS 12 起可用的 Find Windows、Move Window 與 Resize Window 動作。 ↩↩↩