feat(cua): allow persistent consent by executable path

This commit is contained in:
creatixchu committed 2026-10-25 20:28:30 +08:00
1 parent 174b2dbe1b
commit f63da6a3e6
19 files changed
+113 -100

No files matched your search

@@ -9,8 +9,8 @@
en: b32a7ce6483b9b48
zh: ec0938ad032b99d8
/agent-note-application-consent-for-native-computer-use/decision:
en: 1f3eca534d9e5539
zh: 60d8b4185ffd7f54
en: b34d24e39879277e
zh: 03363665d4682ddb
/agent-note-application-consent-for-native-computer-use/alternatives-considered:
en: 18b2c124f1d4e601
zh: 621a6c9512d5612b
@@ -16,7 +16,7 @@ System-permission buttons call the native helper on the DSH host and remain avai
Application identity is owned by [bundle-ID consent](2026-10-25-cua-bundle-id-consent.md), which supersedes this note's original path/signature choice. That original choice prevented copied or replaced applications from inheriting access, at the cost of separate grants per installation and no saved grants for unsigned applications. Enforcement and human decision ownership remain as described here.
Cold application selection prepares an executable identity and handle without launching. The same consent decision guards the private launch continuation, and the helper checks the executable again before launch and binds the resulting process afterward. Selections with a bundle identifier retain that identity after launch; an application without one uses content identity for the pending selection and cannot create a saved grant. Preparing metadata before asking avoids starting an application the user refuses.
Cold application selection prepares an executable identity and handle without launching. The same consent decision guards the private launch continuation, and the helper checks the executable again before launch and binds the resulting process afterward. Selections with a bundle identifier retain that identity after launch; an application without one uses its canonical executable path before and after launch and can save that grant. Preparing metadata before asking avoids starting an application the user refuses.
Conversation decisions survive provider disposal and DSH restart according to [profile consent storage](2026-10-19-cua-profile-consent-storage.md). That decision owns persistence, migration and cross-process revocation. The executor continues to serialize application selections within a conversation, refuse repeated prompts after denial, and give an explicit long-term grant precedence over conversation refusals.
@@ -16,7 +16,7 @@ CUA 执行器在每次对象 API 和原生工具调用时检查应用授权。
应用身份由[Bundle ID 授权](2026-10-25-cua-bundle-id-consent.zh.md)负责,取代本记录原先的路径和签名选择。原选择防止应用副本或替换程序继承许可,代价是各安装副本必须分别授权,未签名应用无法保存长期许可。执行检查和用户决定的所有权仍由本记录规定。
选择尚未运行的应用时,先准备可执行文件身份和句柄,不立即启动。同一授权决定保护内部启动续接,helper 在启动前再次核对可执行文件,并在启动后绑定实际进程。有 Bundle ID 的应用在启动后沿用该身份;没有 Bundle ID 的程序使用本次待启动选择的内容身份,不能生成长期许可。先准备元数据再询问,可以避免启动用户拒绝的应用。
选择尚未运行的应用时,先准备可执行文件身份和句柄,不立即启动。同一授权决定保护内部启动续接,helper 在启动前再次核对可执行文件,并在启动后绑定实际进程。有 Bundle ID 的应用在启动后沿用该身份;没有 Bundle ID 的程序在启动前后使用规范化可执行路径,并可保存该许可。先准备元数据再询问,可以避免启动用户拒绝的应用。
会话决定按[profile 授权存储](2026-10-19-cua-profile-consent-storage.zh.md)的规则,在提供方卸载和 DSH 重启后保留。该记录负责持久化、迁移和跨进程撤销。执行器继续将同一对话对同一应用的选择串行化,拒绝后不再次询问,显式长期许可优先于会话拒绝。
@@ -9,11 +9,11 @@
en: bb387fc89c1c578c
zh: 947456baea45cc95
/agent-note-macos-consent-shared-by-bundle-identifier/decision:
en: 109e84e9bd42b0bf
zh: 18ea0c8be70d248b
en: 36daa80b5b60098c
zh: 3bb53a14d04ed829
/agent-note-macos-consent-shared-by-bundle-identifier/alternatives-considered:
en: 3a81ab1d7f93ecf3
zh: a226ffe1bc0702dd
en: 637b1fd2bafb1b28
zh: 06d882818ffaf800
/agent-note-macos-consent-shared-by-bundle-identifier/consequences:
en: b5e08afa1888180b
zh: c8728a1125f52450
en: 1af6d29a57ff86e2
zh: 32517607b2fc935f
@@ -10,20 +10,24 @@ Users who install multiple versions of one application want one permission decis
## Decision
macOS application consent uses the exact, case-sensitive native bundle identifier. The helper hashes `macos:bundle-id:` followed by that identifier into `CuaApplicationId`. Installation paths, versions and signatures do not distinguish grants. Conversation decisions and saved grants retain the scopes and revocation behavior of [profile consent storage](2026-10-19-cua-profile-consent-storage.md).
macOS application consent uses the exact, case-sensitive native bundle identifier. The helper hashes `macos:bundle-id:` followed by that identifier into `CuaApplicationId`. When a bundle identifier exists, installation paths, versions and signatures do not distinguish grants. Otherwise, the helper hashes `macos:executable-path:` followed by the canonical executable path. Both identities support saved access; path identities survive restart and in-place updates, while a different path requires a new decision. Conversation decisions and saved grants retain the scopes and revocation behavior of [profile consent storage](2026-10-19-cua-profile-consent-storage.md).
This supersedes the executable/signature identity choice in [application consent](2026-09-26-cua-application-consent.md); that note still owns enforcement and human decisions. Running applications without a nonempty bundle identifier retain process-instance consent, while pending launches use executable content identity. Neither can receive an always-allow grant. Native process, executable-location and window checks continue to bind each handle to its selected target. Launch checks reread the bundle identifier and executable path from disk; they do not fingerprint executable content for an application with a bundle identifier. Replacing bytes at the same path under the same identifier therefore retains consent, including during an approval wait.
This supersedes the executable/signature identity choice in [application consent](2026-09-26-cua-application-consent.md); that note still owns enforcement and human decisions. The running process path comes from the operating system. Missing catalog path metadata does not block consent when that process path is available. If the runtime bundle identifier is unavailable, a reported application bundle can supply it only when its main executable matches the process path; otherwise the executable path identifies the grant. Native process, executable-location and window checks continue to bind each handle to its selected target. Launch checks reread the bundle identifier and executable path from disk; they do not fingerprint executable content. Replacing bytes at the same path while retaining the identifier or path identity therefore retains consent, including during an approval wait.
The identity prefix separates new grants from old path/signature digests. Old grants remain removable but do not grant access to new bundle identities; users decide again before permission covers other installations.
The identity prefixes separate new grants from old path/signature, process-instance and executable-content digests. Old grants remain removable but do not grant access to new bundle identities; users decide again before permission covers other installations.
Apple’s [PPPC configuration](https://support.apple.com/guide/deployment/privacy-preferences-policy-control-payload-dep38df53c2a/web) supports bundle-ID or path selectors and separately requires a code-signing requirement. [TN3127](https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements) explains that designated requirements track code identity across versions, while unsigned and ad-hoc code cannot provide the same stability. DSH adopts the selector alternatives for application management without requiring publisher authentication.
## Alternatives considered
**Bundle identifier and path.** This distinguishes installed copies but requires separate decisions for the multiple versions the user wants to authorize together.
**Bundle identifiers only.** Missing identifiers would exclude otherwise usable applications from saved access. Canonical executable paths supply a stable fallback without using a PID or content hash.
**Verified signing identity.** This distinguishes publishers but denies saved access when signing is absent or cannot be verified. Publisher authentication is outside this application-management policy.
## Consequences
Another program declaring the same bundle identifier inherits consent, including a program created or changed through separately authorized model tools. The worst consequence is observing or controlling an application that the user did not approve separately, within its available data and capabilities. The model cannot supply the authorization identity through the CUA request; the native helper reads it from the selected application. Different bundle identifiers require different decisions, even when users consider them versions of one product.
Another program declaring the same bundle identifier or replacing an executable at its approved fallback path inherits consent, including a program created or changed through separately authorized model tools. The worst consequence is observing or controlling an application that the user did not approve separately, within its available data and capabilities. The model cannot supply the authorization identity through the CUA request; the native helper reads it from the selected application. Different bundle identifiers require different decisions, even when users consider them versions of one product.
Revocation applies to every installation sharing the identifier. Tests exercise unsigned copies, different identifiers, missing identifiers, pending and running identity agreement, shared handle revocation and refusal to widen old grants.
Revocation applies to every installation sharing the identifier. Tests exercise unsigned copies, different identifiers, missing identifiers with persistent path consent, process restarts, symlink aliases, pending and running identity agreement, shared handle revocation and refusal to widen old grants.
@@ -10,20 +10,24 @@ Status: implemented
## 决策
macOS 应用授权使用原生应用提供的、区分大小写的完整 Bundle ID。helper 将 `macos:bundle-id:` 和该标识连接后计算摘要,生成 `CuaApplicationId`。安装路径、版本和签名不区分授权。会话决定和长期许可保留[profile 授权存储](2026-10-19-cua-profile-consent-storage.zh.md)规定的范围和撤销行为。
macOS 应用授权使用原生应用提供的、区分大小写的完整 Bundle ID。helper 将 `macos:bundle-id:` 和该标识连接后计算摘要,生成 `CuaApplicationId`。有 Bundle ID 时,安装路径、版本和签名不区分授权。否则,helper 对 `macos:executable-path:` 与规范化可执行路径计算摘要。两种身份都支持长期许可;路径身份在重启和原地更新后保持不变,另一个路径需要新的决定。会话决定和长期许可保留[profile 授权存储](2026-10-19-cua-profile-consent-storage.zh.md)规定的范围和撤销行为。
本决策取代[应用授权](2026-09-26-cua-application-consent.zh.md)中的可执行文件和签名身份选择,原记录仍负责执行检查与用户决定。没有非空 Bundle ID 的运行中应用保留进程实例许可,待启动应用使用可执行文件内容身份,两者都不能获得“始终允许”。原生进程、可执行文件位置和窗口检查继续把每个句柄绑定到所选目标。启动检查从磁盘重新读取 Bundle ID 和可执行路径,但不为具有 Bundle ID 的应用计算可执行内容指纹。因此,在相同路径下替换内容且保留相同标识时,许可仍会沿用,包括等待用户批准期间的替换。
本决策取代[应用授权](2026-09-26-cua-application-consent.zh.md)中的可执行文件和签名身份选择,原记录仍负责执行检查与用户决定。运行进程的可执行路径由操作系统提供。只要能取得实际进程路径,应用目录信息中缺少路径不会阻止授权。运行时无法取得 Bundle ID 时,仅在其报告的应用 bundle 主可执行文件与进程路径一致时读取该标识,否则按可执行路径识别授权。原生进程、可执行文件位置和窗口检查继续把每个句柄绑定到所选目标。启动检查从磁盘重新读取 Bundle ID 和可执行路径,但不计算可执行内容指纹。因此,在相同路径下替换内容且保留相同标识或路径身份时,许可仍会沿用,包括等待用户批准期间的替换。
身份前缀将新授权与旧的路径和签名摘要分开。旧授权仍可撤销,但不能授予新 Bundle ID 身份访问权限;许可覆盖其他安装副本前,需要用户重新决定。
身份前缀将新授权与旧的路径和签名、进程实例、可执行内容摘要分开。旧授权仍可撤销,但不能授予新 Bundle ID 身份访问权限;许可覆盖其他安装副本前,需要用户重新决定。
Apple 的 [PPPC 配置](https://support.apple.com/guide/deployment/privacy-preferences-policy-control-payload-dep38df53c2a/web)支持 Bundle ID 或路径选择条件,同时另行要求代码签名条件。[TN3127](https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements)说明了指定要求如何跨版本识别代码,以及未签名和临时签名代码无法提供同等稳定性。DSH 采用这两种选择条件管理应用,不要求验证发布者身份。
## 考虑过的替代方案
**Bundle ID 加路径。** 可以区分安装副本,但用户希望共同授权的多个版本仍需分别确认。
**仅使用 Bundle ID。** 缺少标识会让本可操作的应用无法获得长期许可。规范化可执行路径提供稳定的后备标识,无须使用进程号或内容摘要。
**验证签名身份。** 可以区分发布者,但没有签名或验签失败时无法保存长期授权。发布者身份验证不属于本项应用管理规则。
## 后果
声明相同 Bundle ID 的其他程序会继承许可,包括通过其他已授权模型工具创建或修改的程序。最坏后果是在应用已有数据和能力范围内,读取或控制用户未单独批准的程序。模型不能在 CUA 请求中提交授权身份,该身份由原生 helper 从实际选中的应用读取。不同 Bundle ID 仍需分别授权,即使用户将它们视为同一产品的不同版本。
其他程序声明相同 Bundle ID,或替换路径授权对应的可执行文件时,会继承许可,包括通过其他已授权模型工具创建或修改的程序。最坏后果是在应用已有数据和能力范围内,读取或控制用户未单独批准的程序。模型不能在 CUA 请求中提交授权身份,该身份由原生 helper 从实际选中的应用读取。不同 Bundle ID 仍需分别授权,即使用户将它们视为同一产品的不同版本。
撤销作用于同一标识的所有安装副本。测试覆盖未签名副本、不同标识、缺少标识、启动前后身份一致、多个句柄共同撤销,以及不扩大旧授权。
撤销作用于同一标识的所有安装副本。测试覆盖未签名副本、不同标识、缺少标识时的持久路径许可、进程重启、符号链接、启动前后身份一致、多个句柄共同撤销,以及不扩大旧授权。
@@ -9,8 +9,8 @@
en: d24696e5f85e9144
zh: 184f7ebd8b8880df
/macos-computer-use-application-identity/change:
en: 0c88539d557dd911
zh: 87bc67ad34f300bd
en: 9ee94f2635015c1c
zh: 679e0f6a410b9728
/macos-computer-use-application-identity/migration:
en: 9c13aeec5e544d36
zh: da697d3ee5a4637f
@@ -9,9 +9,9 @@ English | [中文](guide.zh.md)
## Change
Application grants use the case-sensitive native bundle identifier. Versions and copies at different paths share permission and revocation, regardless of signing. Applications without a bundle identifier remain limited to conversation-scoped access. The profile storage and conversation scopes are unchanged.
Application grants use the case-sensitive native bundle identifier. Versions and copies at different paths share permission and revocation, regardless of signing. Applications without a bundle identifier use their canonical executable path and can also receive always-allow grants. Restarting or replacing the executable at that path retains consent; moving it requires renewed consent. The profile storage and conversation scopes are unchanged.
This affects macOS users with saved path/signature grants, including grants imported from the legacy `allowedApps` configuration. Those grants do not match the new identities. They remain visible and removable but do not authorize other installations automatically. Old and new records can show the same name and path; the permission page does not distinguish them. Another program declaring an approved bundle identifier inherits permission; this policy does not authenticate publishers.
This affects macOS users with saved path/signature grants, including grants imported from the legacy `allowedApps` configuration. Those grants do not match the new identities. They remain visible and removable but do not authorize other installations automatically. Old and new records can show the same name and path; the permission page does not distinguish them. Another program declaring an approved bundle identifier or replacing an executable at an approved fallback path inherits permission; this policy does not authenticate publishers.
## Migration
@@ -9,9 +9,9 @@ description: macOS Computer Use 授权改用 Bundle ID,旧应用身份需要
## 变更
应用授权使用原生应用提供的、区分大小写的 Bundle ID。不同路径的版本和副本共用许可与撤销决定,不受签名状态影响。缺少 Bundle ID 的应用仍仅能获得当前对话范围内的许可。profile 存储和对话范围保持不变。
应用授权使用原生应用提供的、区分大小写的 Bundle ID。不同路径的版本和副本共用许可与撤销决定,不受签名状态影响。缺少 Bundle ID 的应用使用规范化可执行路径,也可以“始终允许”。在该路径重启或替换可执行文件会沿用许可,移动文件则需重新授权。profile 存储和对话范围保持不变。
受影响的是保存过路径和签名授权的 macOS 用户,包括通过旧 `allowedApps` 配置导入授权的用户。这些授权不匹配新身份。旧记录仍可查看和撤销,但不会自动授权其他安装副本。新旧条目可能显示相同名称和路径,权限页没有区分它们。其他程序声明已批准的 Bundle ID 时也会继承许可,这项规则不验证发布者身份。
受影响的是保存过路径和签名授权的 macOS 用户,包括通过旧 `allowedApps` 配置导入授权的用户。这些授权不匹配新身份。旧记录仍可查看和撤销,但不会自动授权其他安装副本。新旧条目可能显示相同名称和路径,权限页没有区分它们。其他程序声明已批准的 Bundle ID,或替换路径授权对应的可执行文件时,也会继承许可,这项规则不验证发布者身份。
## 迁移
@@ -15,8 +15,8 @@
en: ce05c98afbe694b4
zh: d4e30ee79d4cb08f
/deepseek-ai-dsh-experimental-cua-macos/use-this-package:
en: a8e172924a84b626
zh: 792131b453baae8d
en: f309b70a6f669ae0
zh: 1fe4c4ba0a7c3045
/deepseek-ai-dsh-experimental-cua-macos/use-this-package/connect-an-external-execution-environment:
en: 786b8fd630b11e5f
zh: 55143f15486afce8
@@ -45,8 +45,8 @@
en: a2c9acb5a4320dda
zh: e8d1586af60215c4
/deepseek-ai-dsh-experimental-cua-macos/known-limitations-and-deferred-work:
en: 512a12f40f86f4cf
zh: 8738c47edea0f4d3
en: 93de44ec35281188
zh: 80405135f3e1669a
/deepseek-ai-dsh-experimental-cua-macos/known-limitations-and-deferred-work/dev-note:
en: b836f01bd0bceb4a
zh: 8122fa08b2aa95fa
+3 -3
View File
@@ -40,7 +40,7 @@ export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native"
pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.yml --dump-config
```
The dump command checks profile composition without a model request. For a real task, replace `--dump-config` with a prompt and configure the profile’s model credentials. The overlay mounts the computer-use registry and this provider; remove an existing competing provider before using it. The responsible host/helper needs Accessibility permission for AX/input and Screen Recording permission for capture. `getCapabilities` reports permissions and session lock state without opening a system dialog. Locked or unavailable desktop sessions reject UI observations and input before dispatch; the user must unlock macOS before resuming. `listApps` and `cua.getState` list running applications. `findApps(query)` searches standard macOS application directories by name, bundle ID or absolute path. `getApp` selects a unique name, bundle ID, path or running PID, obtains application consent, and launches a stopped app in the background when needed. A stopped application’s bundle identifier and executable path are read from disk before approval and rechecked before and after launch; applications without a bundle identifier only receive session access for the selected instance. The helper waits for the launched application to finish launching before returning its PID. The returned object reads initial AX without activating the app. Launch cancellation may leave the application running. Model guidance starts with background observation and input, without activation during initialization or before each action. For hidden or minimized windows, try `app.show()` or `window.show()` and read fresh state. This requests unhide and unminimize without activation, raising or switching Spaces; app-level restoration selects the focused, main or sole AX window. The result names the selected `windowId`, or null when no window was selected, and reports `foregroundChanged` by comparing the front app and top window before and after the request. Null means foreground evidence was unavailable; later focus changes remain possible. If restoration fails or the window remains unusable, explain the needed foreground step, call `app.activate()` or an observed window’s `raise` action, then observe again. Necessary foreground access takes priority over preserving focus. Applications may change focus after input; background delivery does not guarantee an undisturbed user session.
The dump command checks profile composition without a model request. For a real task, replace `--dump-config` with a prompt and configure the profile’s model credentials. The overlay mounts the computer-use registry and this provider; remove an existing competing provider before using it. The responsible host/helper needs Accessibility permission for AX/input and Screen Recording permission for capture. `getCapabilities` reports permissions and session lock state without opening a system dialog. Locked or unavailable desktop sessions reject UI observations and input before dispatch; the user must unlock macOS before resuming. `listApps` and `cua.getState` list running applications. `findApps(query)` searches standard macOS application directories by name, bundle ID or absolute path. `getApp` selects a unique name, bundle ID, path or running PID, obtains application consent, and launches a stopped app in the background when needed. A stopped application’s bundle identifier and executable path are read from disk before approval and rechecked before and after launch; applications without a bundle identifier use their canonical executable path for reusable consent. The helper waits for the launched application to finish launching before returning its PID. The returned object reads initial AX without activating the app. Launch cancellation may leave the application running. Model guidance starts with background observation and input, without activation during initialization or before each action. For hidden or minimized windows, try `app.show()` or `window.show()` and read fresh state. This requests unhide and unminimize without activation, raising or switching Spaces; app-level restoration selects the focused, main or sole AX window. The result names the selected `windowId`, or null when no window was selected, and reports `foregroundChanged` by comparing the front app and top window before and after the request. Null means foreground evidence was unavailable; later focus changes remain possible. If restoration fails or the window remains unusable, explain the needed foreground step, call `app.activate()` or an observed window’s `raise` action, then observe again. Necessary foreground access takes priority over preserving focus. Applications may change focus after input; background delivery does not guarantee an undisturbed user session.
| Field | Default | Meaning |
| --- | --- | --- |
@@ -67,7 +67,7 @@ Keypad names (`KP_0` through `KP_9`, `KP_Enter`, and keypad operators) retain th
Agent disposal and plugin unload cancel and await the complete tool call, including image admission and attachment storage. An image write already in progress must settle before disposal returns; a canceled call cannot return a successful screenshot.
Application consent is enforced before AX reads, window-title reads, screenshots, activation, and input through the CUA tool. The first selection asks the human for session access, denial, or saved access when the application has a nonempty bundle identifier. Session access is stored by conversation and application identity and survives DSH restart and Computer Use disablement. Other conversations require their own consent. Application identities use the case-sensitive bundle identifier, so versions and copies at different paths share consent and revocation regardless of signing. Applications without a bundle identifier remain bound to their process instance. This policy does not authenticate the publisher: another program declaring the same identifier inherits consent. Denial prevents repeated prompts in that session. Revocation reads and removes saved access in the same queue as grant creation, invalidates each affected Agent connection, and awaits its native cleanup without replaying input. Every app handle on those Agent connections becomes unusable; other applications need fresh selection, but their session and saved grants remain reusable. See the [application consent decision](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md). These checks govern CUA calls; persistent PTC and separately authorized shell tools are not an operating-system sandbox.
Application consent is enforced before AX reads, window-title reads, screenshots, activation, and input through the CUA tool. The first selection asks the human for session access, denial, or saved access when profile persistence is available. Session access is stored by conversation and application identity and survives DSH restart and Computer Use disablement. Other conversations require their own consent. Application identities use the case-sensitive bundle identifier, so versions and copies at different paths share consent and revocation regardless of signing. Without a bundle identifier, consent uses the canonical executable path. Restarting or updating at that path retains consent; moving or copying the executable to another path requires a new decision. This policy does not authenticate the publisher: another program declaring the same identifier, or replacing an approved executable at the same fallback path, inherits consent. Denial prevents repeated prompts in that session. Revocation reads and removes saved access in the same queue as grant creation, invalidates each affected Agent connection, and awaits its native cleanup without replaying input. Every app handle on those Agent connections becomes unusable; other applications need fresh selection, but their session and saved grants remain reusable. See the [application consent decision](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md). These checks govern CUA calls; persistent PTC and separately authorized shell tools are not an operating-system sandbox.
Raw `getState` exposes validated state fields and the complete text for the requested disclosure options. The native helper also returns structured element diagnostics for native checks; the tool projection omits that duplicate array from model text and structured tool values.
@@ -220,7 +220,7 @@ The queue orders individual native requests. Holding an app object does not rese
- Observed failures include dispatched Unicode text not appearing in Blender and background Command+A not selecting all text in AppKit. Inspect application state after input; a successful tool result confirms dispatch only.
- Native binaries require a local build. Signing and Desktop distribution remain future work. The [permission page](../cua/README.md) offers macOS prompts and app revocation; packaged Desktop permission attribution remains deferred.
- Saved permission rows show the name, path and icon of one approved copy, not an inventory of every installation sharing its identity. Removing that copy can leave its icon unavailable. Revocation still applies to every copy with the same identity.
- If macOS does not expose a running application’s bundle identifier, that process receives only instance-scoped consent. A cold launch approved under a bundle identifier rejects with `STALE_APP` if the running process cannot confirm it.
- If macOS does not expose a running application’s bundle identifier, the helper checks its reported bundle only when its executable matches the observed process path, then falls back to that path. Failure to identify the actual process executable remains an error; missing bundle metadata alone does not prevent saved consent.
- Persistent PTC is an external dependency assumption, not an implementation in this package. Real-model task success requires separate evaluation beyond native and protocol fixtures.
-----
+3 -3
View File
@@ -40,7 +40,7 @@ export DSH_CUA_HELPER="$cua_native_root/.build/release/dsh-cua-native"
pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.yml --dump-config
```
配置输出命令不发起模型请求。执行真实任务时,将 `--dump-config` 替换为任务提示,并配置该 profile 的模型凭据。配置挂载 computer-use 注册表和本 provider;使用前需移除已有的竞争 provider。负责执行的宿主或辅助进程需要辅助功能权限来读取 AX 和输入,需要屏幕录制权限来截图。`getCapabilities` 查询权限和锁屏状态,不弹出系统授权窗口。桌面会话锁定或不可用时,界面观察与输入会在分派前拒绝,需由用户解锁 macOS 后继续。`listApps` 和 `cua.getState` 列出运行中的应用。`findApps(query)` 按名称、bundle ID 或绝对路径搜索 macOS 标准应用目录。`getApp` 按唯一名称、bundle ID、路径或运行中的 PID 选择应用,取得应用许可后,在需要时后台启动尚未运行的应用。授权前从磁盘读取应用的 Bundle ID 和可执行路径,在启动前后重新读取并核对;没有 Bundle ID 的应用只能取得所选实例的会话许可。helper 等待应用完成启动后才返回其 PID。返回的对象读取初始 AX,不自动激活应用。启动被取消时,应用可能已经运行。模型指引要求先在后台观察和输入,不在初始化或每次动作前激活应用。窗口隐藏或最小化时,先尝试 `app.show()` 或 `window.show()`,再重新观察。它请求取消隐藏和恢复最小化,不主动激活、抬起窗口或切换桌面空间;应用级恢复选择焦点窗口、主窗口或唯一 AX 窗口。结果通过 `windowId` 指明选中的窗口,未选中时为 null;`foregroundChanged` 比较操作前后的前台应用和最上层窗口,无法取得完整观察时为 null,返回后仍可能发生焦点变化。恢复失败或窗口仍不可用时,说明需要前台的步骤,再调用 `app.activate()` 或窗口的 `raise` 动作并重新观察。必须使用前台时,以完成任务为先。应用收到输入后仍可能改变焦点,后台投递不保证用户操作始终不受干扰。
配置输出命令不发起模型请求。执行真实任务时,将 `--dump-config` 替换为任务提示,并配置该 profile 的模型凭据。配置挂载 computer-use 注册表和本 provider;使用前需移除已有的竞争 provider。负责执行的宿主或辅助进程需要辅助功能权限来读取 AX 和输入,需要屏幕录制权限来截图。`getCapabilities` 查询权限和锁屏状态,不弹出系统授权窗口。桌面会话锁定或不可用时,界面观察与输入会在分派前拒绝,需由用户解锁 macOS 后继续。`listApps` 和 `cua.getState` 列出运行中的应用。`findApps(query)` 按名称、bundle ID 或绝对路径搜索 macOS 标准应用目录。`getApp` 按唯一名称、bundle ID、路径或运行中的 PID 选择应用,取得应用许可后,在需要时后台启动尚未运行的应用。授权前从磁盘读取应用的 Bundle ID 和可执行路径,在启动前后重新读取并核对;没有 Bundle ID 的应用使用规范化的可执行路径复用授权。helper 等待应用完成启动后才返回其 PID。返回的对象读取初始 AX,不自动激活应用。启动被取消时,应用可能已经运行。模型指引要求先在后台观察和输入,不在初始化或每次动作前激活应用。窗口隐藏或最小化时,先尝试 `app.show()` 或 `window.show()`,再重新观察。它请求取消隐藏和恢复最小化,不主动激活、抬起窗口或切换桌面空间;应用级恢复选择焦点窗口、主窗口或唯一 AX 窗口。结果通过 `windowId` 指明选中的窗口,未选中时为 null;`foregroundChanged` 比较操作前后的前台应用和最上层窗口,无法取得完整观察时为 null,返回后仍可能发生焦点变化。恢复失败或窗口仍不可用时,说明需要前台的步骤,再调用 `app.activate()` 或窗口的 `raise` 动作并重新观察。必须使用前台时,以完成任务为先。应用收到输入后仍可能改变焦点,后台投递不保证用户操作始终不受干扰。
| 字段 | 默认值 | 含义 |
| --- | --- | --- |
@@ -67,7 +67,7 @@ pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.
Agent 销毁和插件卸载会取消并等待完整工具调用,包括图像接入检查和附件存储。已经开始的图像写入必须结束后,销毁才会返回;已取消的调用不会返回成功截图。
CUA 工具在读取 AX、窗口标题、截图、激活和输入前检查应用授权。首次选择应用时询问用户,可选择本会话允许、拒绝;具有非空 Bundle ID 的应用还可保存长期授权。会话许可按对话和应用身份持久化,重启 DSH 或关闭 Computer Use 后仍可复用,其他对话需要单独授权。应用身份使用区分大小写的 Bundle ID,因此不同路径下的版本和副本共用许可与撤销决定,不受签名状态影响。没有 Bundle ID 的应用仍绑定进程实例。这项规则不验证发布者身份,其他程序声明相同标识时也会继承许可。拒绝后,同一会话不会反复询问。撤销与长期许可的保存共用队列,在队列内读取并移除许可,然后使相关 Agent 连接失效并等待原生清理完成,不自动重放输入。同一 Agent 连接中的其他应用也需要重新选择,其他应用的会话许可和长期许可仍可复用。设计理由见[应用授权决策](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md)。这些检查的范围是 CUA 调用。persistent PTC 与已授权的 shell 仍按各自执行权限运行。
CUA 工具在读取 AX、窗口标题、截图、激活和输入前检查应用授权。首次选择应用时询问用户,可选择本会话允许、拒绝;profile 提供持久存储时还可保存长期授权。会话许可按对话和应用身份持久化,重启 DSH 或关闭 Computer Use 后仍可复用,其他对话需要单独授权。应用身份使用区分大小写的 Bundle ID,因此不同路径下的版本和副本共用许可与撤销决定,不受签名状态影响。没有 Bundle ID 时,授权使用规范化的可执行路径。在同一路径下重启或更新会沿用许可,移动或复制到另一个路径则需要重新决定。这项规则不验证发布者身份,其他程序声明相同标识,或替换路径授权所对应的可执行文件时,也会继承许可。拒绝后,同一会话不会反复询问。撤销与长期许可的保存共用队列,在队列内读取并移除许可,然后使相关 Agent 连接失效并等待原生清理完成,不自动重放输入。同一 Agent 连接中的其他应用也需要重新选择,其他应用的会话许可和长期许可仍可复用。设计理由见[应用授权决策](../../../.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md)。这些检查的范围是 CUA 调用。persistent PTC 与已授权的 shell 仍按各自执行权限运行。
原始 `getState` 操作返回经过校验的状态字段,以及按本次披露选项生成的完整文本。原生 helper 还返回供原生检查使用的结构化元素诊断数组;工具投影会从模型文本和结构化工具值中移除这份重复数组。
@@ -220,7 +220,7 @@ Use selectText only on controls advertising selectionWritable. It selects a uniq
- 实测 Blender Unicode 输入曾出现分派成功但界面未显示文本,后台 AppKit 的 Command+A 曾未选中全文。输入后必须检查应用状态;工具返回成功只表示已分派。
- 原生二进制需要本地构建,签名和桌面分发留待后续。[权限页面](../cua/README.zh.md)提供 macOS 申请入口和应用授权撤销;桌面安装包的权限归属留待后续处理。
- 已保存许可的条目显示一个已批准副本的名称、路径和图标,不枚举共享身份的所有安装副本。删除该副本后图标可能不可用,撤销仍作用于同一身份的所有副本。
- macOS 无法提供运行中应用的 Bundle ID 时,该进程仅获得实例范围内的许可。冷启动按 Bundle ID 获批后,如果运行进程无法确认该标识,会返回 `STALE_APP`。
- macOS 无法提供运行中应用的 Bundle ID 时,helper 仅在其报告的 bundle 可执行路径与实际进程路径相同时读取该 bundle,仍无标识则使用路径。无法识别实际进程的可执行路径时仍会报错;仅缺少 bundle 元数据不会阻止保存许可。
- 持久 PTC 是对外部运行环境的假设,本包不实现它。真实模型任务成功率需要在原生测试和协议测试之外单独评估。
-----
@@ -18,14 +18,14 @@
en: 02b25ac1126b6094
zh: 3995a1f46354531b
/dsh-cua-native-helper/request-protocol:
en: 48759123ceefb61e
zh: 23f43ee10b2e46ff
en: c4c32b6447e39e00
zh: e798843da1a83ada
/dsh-cua-native-helper/observation-and-input:
en: c527eedc3a0ee968
zh: ce8cc12d5a62e8ae
/dsh-cua-native-helper/verification:
en: e60dacfab2ae1e63
zh: 82ee64ec031f4c6c
en: a40f5a47ea65d04b
zh: deb40dceac90d311
/dsh-cua-native-helper/further-exploration:
en: 265e1927952ce6d7
zh: 7c35d9fd98b6e6c8
@@ -57,7 +57,7 @@ Every app-targeted operation requires `appHandle` from `getApp` in the same help
Stable error codes are `INVALID_REQUEST` (malformed envelope), `INVALID_ARGUMENT` (invalid parameters), `UNSUPPORTED` (unknown operation), `APP_UNAVAILABLE`, `AMBIGUOUS_APP`, `STALE_APP`, `STALE_SNAPSHOT`, `WINDOW_UNAVAILABLE`, `WINDOW_CHANGED`, `PERMISSION_DENIED`, `FOREGROUND_REQUIRED` (hover target is not the unobscured foreground window), `PANEL_NOT_READY` (system panel recipient unavailable before input), and `NATIVE_ERROR` (remaining operating-system failures). An error never promises that a partially dispatched input was rolled back. The host must not retry input automatically.
Application selection returns an authorization record to the Host: a SHA-256 digest of the case-sensitive bundle identifier prefixed with `macos:bundle-id:`, display name, executable path, and persistable flag. All installations with that identifier share consent, including unsigned and ad-hoc-signed applications. Without a nonempty bundle identifier, running instances use process-lifetime identity and stopped applications use an executable digest; neither receives saved consent. The Host strips this record from model results and obtains human consent before launch, observation or input. This pipe protocol is trusted Host transport, not a sandbox against a separately authorized shell process.
Application selection returns an authorization record to the Host: a SHA-256 digest of the case-sensitive bundle identifier prefixed with `macos:bundle-id:`, display name, executable path, and persistable flag. All installations with that identifier share consent, including unsigned and ad-hoc-signed applications. Without a nonempty bundle identifier, both running and stopped applications use SHA-256 of `macos:executable-path:` followed by the symlink-resolved executable path and remain eligible for saved consent. Process IDs and start times validate live handles but do not enter the saved identity. The Host strips this record from model results and obtains human consent before launch, observation or input. This pipe protocol is trusted Host transport, not a sandbox against a separately authorized shell process.
Protocol declarations are generated from [the shared protocol](../../cua/src/protocol.ts). Run `pnpm exec tsx packages/experimental/cua-macos/native/generate-protocol.ts` from the repository root; package tests verify generated JSON and Swift field/enum tables. Model-submittable parameters exclude Host resource bounds and the private launch continuation.
@@ -105,7 +105,7 @@ Use `python3 packages/experimental/cua-macos/native/tests/test_swift.py` for off
Run `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper` to verify concurrent startup, a shared service PID, independent handles, same-application handoff, request-local screenshot deadlines, connection cleanup and signal shutdown with an owned AppKit fixture. Existing Accessibility and Screen Recording grants are required. `--without-desktop` limits the check to protocol and process lifetime.
Run `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper` in an unlocked macOS GUI session with the Apple Swift compiler available. It creates and cleans up its own AppKit bundles, rejects changed bundle metadata before launch, checks cold/running identity agreement, and runs two same-ID copies together. It neither reads AX nor captures screens or sends input, so it does not require Accessibility or Screen Recording grants.
Run `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper` in an unlocked macOS GUI session with the Apple Swift compiler available. It creates and cleans up its own AppKit bundles, rejects changed bundle metadata before launch, checks cold/running identity agreement, runs two same-ID copies together, and confirms that an application without a bundle identifier can use persistent path consent. It neither reads AX nor captures screens or sends input, so it does not require Accessibility or Screen Recording grants.
Run `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py --helper /absolute/helper` in a GUI login session to verify a Launch Services application that starts a nested GUI launcher and then the helper. The helper identifies the outer responsible application and retains its name and icon after the launchers exit. This check owns both applications and the helper, cleans up its Launch Services registrations, and does not request Accessibility or Screen Recording access.
@@ -57,7 +57,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后
稳定错误码包括 `INVALID_REQUEST`(信封无效)、`INVALID_ARGUMENT`(参数无效)、`UNSUPPORTED`(操作未知)、`APP_UNAVAILABLE`、`AMBIGUOUS_APP`、`STALE_APP`、`STALE_SNAPSHOT`、`WINDOW_UNAVAILABLE`、`WINDOW_CHANGED`、`PERMISSION_DENIED`、`FOREGROUND_REQUIRED`(悬停目标不是未被遮挡的前台窗口)、`PANEL_NOT_READY`(输入前系统面板接收方尚不可用)及 `NATIVE_ERROR`(其余操作系统失败)。错误不保证已分派的部分输入被回滚,Host 不得自动重试输入。
应用选择向 Host 返回授权记录:区分大小写的 Bundle ID 加上 `macos:bundle-id:` 前缀后计算的 SHA-256 摘要、显示名称、可执行路径和可持久化标记。相同标识的所有安装副本共用授权,包括未签名和临时签名的应用。缺少非空 Bundle ID 时,运行中实例使用进程生命期身份,未启动应用使用可执行文件摘要,两者都不能获得长期授权。Host 从模型结果中移除此记录,并在启动、观察和输入前取得用户许可。此管道协议用于可信 Host 传输,无法隔离另一个已获授权的 shell 进程。
应用选择向 Host 返回授权记录:区分大小写的 Bundle ID 加上 `macos:bundle-id:` 前缀后计算的 SHA-256 摘要、显示名称、可执行路径和可持久化标记。相同标识的所有安装副本共用授权,包括未签名和临时签名的应用。缺少非空 Bundle ID 时,运行中和未启动应用都对 `macos:executable-path:` 与解析符号链接后的可执行路径计算 SHA-256,仍可保存长期授权。进程号和启动时间只校验存活句柄,不进入已保存身份。Host 从模型结果中移除此记录,并在启动、观察和输入前取得用户许可。此管道协议用于可信 Host 传输,无法隔离另一个已获授权的 shell 进程。
协议声明从[共享协议](../../cua/src/protocol.ts)生成。在仓库根目录运行 `pnpm exec tsx packages/experimental/cua-macos/native/generate-protocol.ts`,包测试检查生成的 JSON 和 Swift 字段/枚举表。模型参数不包含 Host 资源上限和私有启动续接操作。
@@ -105,7 +105,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后
运行 `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper`,通过测试自建的 AppKit fixture 核实并发启动、共享服务 PID、独立句柄、同一应用交接、请求级截图期限、连接清理和信号退出。需要已有辅助功能和屏幕录制权限。`--without-desktop` 将检查限定为协议与进程生命期。
在已解锁的 macOS GUI 会话中运行 `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper`,需要可用的 Apple Swift 编译器。它创建并清理自己的 AppKit 应用,验证启动前拒绝已改变的应用元数据、启动前后身份一致,以及同 ID 的两个副本同时运行。测试不读取 AX、不截图、不发送输入,因此不需要辅助功能或屏幕录制权限。
在已解锁的 macOS GUI 会话中运行 `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper`,需要可用的 Apple Swift 编译器。它创建并清理自己的 AppKit 应用,验证启动前拒绝已改变的应用元数据、启动前后身份一致,同 ID 的两个副本同时运行,以及缺少 Bundle ID 的应用可以获得持久路径许可。测试不读取 AX、不截图、不发送输入,因此不需要辅助功能或屏幕录制权限。
在 GUI 登录会话中运行 `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py --helper /absolute/helper`,验证由 Launch Services 启动的应用先启动另一个 GUI 启动器,再启动 helper。helper 识别外层责任应用,并在启动器退出后保留其名称和图标。检查只使用测试自建的两个应用和 helper,结束后清理对应的 Launch Services 注册,不申请辅助功能或屏幕录制权限。
@@ -1,4 +1,4 @@
// Consent follows the native bundle identifier; handles still identify one process instance.
// Consent follows the bundle identifier or executable path; handles identify one process instance.
import AppKit
import CryptoKit
import Foundation
@@ -7,21 +7,21 @@ enum ApplicationAuthorization {
// Versions and copies with the same bundle identifier share consent, regardless of signing.
static func describe(_ identity: ProcessIdentity, name: String, expectedExecutablePath: String?) throws -> JSONValue {
try NativePlatform.verify(identity)
guard let executablePath = NativePlatform.executablePath(identity.pid), let expectedExecutablePath else {
guard let executablePath = NativePlatform.executablePath(identity.pid) else {
throw NativeFailure(code: "APP_UNAVAILABLE", message: "Cannot identify the application executable")
}
let path = URL(fileURLWithPath: executablePath).resolvingSymlinksInPath().path
guard path == URL(fileURLWithPath: expectedExecutablePath).resolvingSymlinksInPath().path else {
if let expectedExecutablePath, path != URL(fileURLWithPath: expectedExecutablePath).resolvingSymlinksInPath().path {
throw NativeFailure(code: "STALE_APP", message: "Application executable changed during selection; select the application again")
}
let persistentID = bundleIdentity(NSRunningApplication(processIdentifier: identity.pid)?.bundleIdentifier)
let process = "process:\(identity.pid):\(identity.startedSeconds):\(identity.startedMicroseconds)"
let id = persistentID ?? digest(path + "\n" + process)
let application = NSRunningApplication(processIdentifier: identity.pid)
var bundleID = application?.bundleIdentifier
if bundleID?.isEmpty != false, let url = application?.bundleURL,
let installed = InstalledApplication.read(url), installed.executablePath == path {
bundleID = installed.bundleID
}
try NativePlatform.verify(identity)
return .object([
"id": .string(id), "name": .string(name), "executablePath": .string(path),
"persistable": .bool(persistentID != nil),
])
return record(bundleID: bundleID, name: name, executablePath: path)
}
static func describeInstalled(_ app: InstalledApplication) throws -> JSONValue {
@@ -29,24 +29,16 @@ enum ApplicationAuthorization {
current.bundleID == app.bundleID, current.executablePath == app.executablePath else {
throw NativeFailure(code: "STALE_APP", message: "Application changed after selection; select it again")
}
let persistentID = bundleIdentity(app.bundleID)
let id: String
if let persistentID { id = persistentID }
else {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: app.executablePath))
defer { try? handle.close() } // FileHandle also owns closure if an OS close error is reported.
var hash = SHA256()
while let data = try handle.read(upToCount: 64 * 1024), !data.isEmpty { hash.update(data: data) }
let executable = "executable:" + hash.finalize().map { String(format: "%02x", $0) }.joined()
id = digest(app.executablePath + "\n" + executable)
}
return .object(["id": .string(id), "name": .string(app.name), "executablePath": .string(app.executablePath),
"persistable": .bool(persistentID != nil)])
return record(bundleID: current.bundleID, name: app.name, executablePath: current.executablePath)
}
private static func bundleIdentity(_ identifier: String?) -> String? {
guard let identifier, !identifier.isEmpty else { return nil }
return digest("macos:bundle-id:" + identifier)
private static func record(bundleID: String?, name: String, executablePath: String) -> JSONValue {
let path = URL(fileURLWithPath: executablePath).resolvingSymlinksInPath().path
let identity: String
if let bundleID, !bundleID.isEmpty { identity = "macos:bundle-id:" + bundleID }
else { identity = "macos:executable-path:" + path }
return .object(["id": .string(digest(identity)), "name": .string(name),
"executablePath": .string(path), "persistable": .bool(true)])
}
private static func digest(_ value: String) -> String {
@@ -35,9 +35,11 @@ def main():
executable.parent.mkdir(parents=True)
shutil.copy2(binary, executable)
state = root / (name + ".json")
info = {"CFBundleIdentifier": bundle_id, "CFBundleName": name, "CFBundleDisplayName": name,
info = {"CFBundleName": name, "CFBundleDisplayName": name,
"CFBundleExecutable": "fixture", "CFBundlePackageType": "APPL",
"DSHFixtureStatePath": str(state), "DSHFixtureBackground": True}
if bundle_id is not None:
info["CFBundleIdentifier"] = bundle_id
(app / "Contents/Info.plist").write_bytes(plistlib.dumps(info))
cleanup.callback(subprocess.run, [register, "-u", str(app)], check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
@@ -72,7 +74,7 @@ def main():
"The replaced application is running after refusal"
identities = []
for name, bundle_id in [("Version One", identifier), ("Version Two", identifier), ("Other", identifier + ".other")]:
for name, bundle_id in [("Version One", identifier), ("Version Two", identifier), ("Other", identifier + ".other"), ("No identifier", None)]:
app, executable, state, info = fixture(name, bundle_id)
pending = helper.call("getApp", {"selector": str(app)})
assert pending["pid"] is None and not state.exists(), pending
@@ -86,9 +88,12 @@ def main():
assert Path(running["authorization"]["executablePath"]).samefile(executable), running
assert Path(pending["authorization"]["executablePath"]).samefile(executable), pending
identities.append(running["authorization"]["id"])
if bundle_id is None:
expected_path = hashlib.sha256(("macos:executable-path:" + str(executable.resolve())).encode()).hexdigest()
assert running["authorization"]["id"] == expected_path, running
assert identities[0] == identities[1] == expected, identities
assert identities[2] != expected, identities
print("PASS: changed bundles cannot launch; cold/running identities agree; copies share consent; other IDs stay separate")
print("PASS: changed bundles cannot launch; cold/running identities agree; copies share consent; other IDs stay separate; missing IDs use persistent paths")
if __name__ == "__main__":
@@ -48,15 +48,21 @@ private func fixtureApplication(_ directory: URL, name: String, identifier: Stri
}
}
@Test(arguments: [nil, ""] as [String?]) func unidentifiedPendingIdentityChangesWhenTheExecutableChanges(identifier: String?) throws {
@Test(arguments: [nil, ""] as [String?]) func applicationsWithoutBundleIdentifiersRetainPathConsentAcrossUpdates(identifier: String?) throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent("dsh-cua-apps-" + UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
let app = try fixtureApplication(root, name: "Fixture", identifier: identifier)
let before = try ApplicationAuthorization.describeInstalled(app)
#expect(before["persistable"] == .bool(false))
#expect(before["persistable"] == .bool(true))
try Data("replacement executable".utf8).write(to: URL(fileURLWithPath: app.executablePath))
let after = try ApplicationAuthorization.describeInstalled(app)
#expect(before["id"] != after["id"])
#expect(before["id"] == after["id"])
let copy = try fixtureApplication(root, name: "Other installation", identifier: identifier)
#expect(try ApplicationAuthorization.describeInstalled(copy)["id"] != before["id"])
let alias = root.appendingPathComponent("Alias.app")
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: URL(fileURLWithPath: app.path))
let linked = try #require(InstalledApplication.read(alias))
#expect(try ApplicationAuthorization.describeInstalled(linked)["id"] == before["id"])
}
@Test func unsignedVersionsShareConsentOnlyForTheSameBundleIdentifier() throws {
@@ -15,14 +15,12 @@ import Testing
}
}
@Test func refusesApplicationConsentWithoutAnExecutableLocation() throws {
@Test func readsTheProcessPathWhenTheCatalogHasNoExecutableLocation() throws {
let identity = try NativePlatform.identity(getpid())
do {
_ = try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: nil)
Issue.record("Missing executable metadata was accepted")
} catch let error as NativeFailure {
#expect(error.code == "APP_UNAVAILABLE")
}
let path = try #require(NativePlatform.executablePath(identity.pid))
let observed = try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: nil)
#expect(observed["persistable"] == .bool(true))
#expect(try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: path) == observed)
}
@MainActor @Test func applicationIconsUseBoundedNativePNGs() throws {
@@ -35,24 +33,28 @@ import Testing
#expect(encoded.utf8.count <= 65536)
}
@Test func unbundledRunningProcessesCannotReceivePersistentConsent() throws {
let process = Process()
let input = Pipe()
process.executableURL = URL(fileURLWithPath: "/bin/cat")
process.standardInput = input
process.standardOutput = FileHandle.nullDevice
try process.run()
defer {
input.fileHandleForWriting.closeFile()
if process.isRunning { process.terminate() }
process.waitUntilExit()
@Test func unbundledRunningProcessesReceivePersistentPathConsent() throws {
var records: [JSONValue] = []
for _ in 0..<2 {
let process = Process()
let input = Pipe()
process.executableURL = URL(fileURLWithPath: "/bin/cat")
process.standardInput = input
process.standardOutput = FileHandle.nullDevice
try process.run()
defer {
input.fileHandleForWriting.closeFile()
if process.isRunning { process.terminate() }
process.waitUntilExit()
}
let identity = try NativePlatform.identity(process.processIdentifier)
let executable = try #require(NativePlatform.executablePath(identity.pid))
let authorization = try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: executable)
#expect(authorization["persistable"] == .bool(true))
#expect(authorization["id"].string?.count == 64)
records.append(authorization)
}
let identity = try NativePlatform.identity(process.processIdentifier)
let executable = try #require(NativePlatform.executablePath(identity.pid))
let authorization = try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: executable)
#expect(authorization["persistable"] == .bool(false))
#expect(authorization["id"].string?.count == 64)
#expect(try ApplicationAuthorization.describe(identity, name: "Fixture", expectedExecutablePath: executable) == authorization)
#expect(records[0] == records[1])
}
@Test func resolvesTheCurrentProcessResponsibility() throws {