refactor(cua): keep native permission guidance in macOS provider

This commit is contained in:
creatixchu
2026-10-30 14:11:06 +08:00
parent cbbf9d86a1
commit 44fc4192e0
36 changed files with 330 additions and 192 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/computer-use.md
/computer-use:
en: f0a1e4ef2656deb9
zh: b00aad01231cf8ad
en: 38cfa2a698be898f
zh: 60961256fee5ef21
/computer-use/choose-a-provider:
en: fa2e0d7b41c0450b
zh: af9a799711f901e6
+6 -8
View File
@@ -14,7 +14,6 @@ The CUA provider owns `ctx.cuaAccess` for system-permission status, explicit sys
|---|---|
| `CuaApplicationId`, `CuaApplicationGrant` | A platform-provided application identity and its displayed saved decision. |
| `CuaSystemPermission` | Accessibility or Screen Recording, selected by the human. |
| `CuaPermissionGuideText` | Client-localized native guide instructions and recovery messages; the helper supplies the application name. |
| `CuaPermissionProcess`, `CuaSystemPermissionState` | Native privilege checks, observed helper/parent processes, and the application identified by macOS process responsibility at helper startup. The independent helper uses its own bundle when system attribution is unavailable; application metadata is absent if neither bundle can be identified. |
| `CuaApplicationState` | Saved grants, refused applications, and whether persistence is available. Session-only grants remain internal to the provider. System observations and application icons are queried separately so the page can display each result as it arrives. |
@@ -104,21 +103,20 @@ Browser controls for provider-owned native permission and application decisions.
@Remote async applicationIcons(): Promise<Record<string, string>>
/**
* Open the selected System Settings pane; show the native drag guide only for a missing privilege.
* Guide preparation failures still attempt settings navigation before reporting the failure.
* Request the selected system privilege through the native provider.
* @param permission macOS privilege to request.
* @param text Localized guide copy; application identity comes from the native helper.
* @returns Current permission observations; opening the guide does not grant permission.
* @param locale Current Client language for provider-owned permission UI.
* @returns Current permission observations; requesting does not establish consent.
*/
@Remote async requestPermission(permission: CuaSystemPermission, text: CuaPermissionGuideText): Promise<CuaSystemPermissionState>
@Remote async requestPermission(permission: CuaSystemPermission, locale: string): Promise<CuaSystemPermissionState>
/**
* Open the macOS privacy pane selected by the user.
* @param permission Corresponding system privilege.
* @param failureMessage Localized manual navigation instructions if macOS rejects the open request.
* @param locale Current Client language for provider-owned recovery instructions.
* @returns Completion after macOS accepted the open request.
*/
@Remote async openSettings(permission: CuaSystemPermission, failureMessage: string): Promise<void>
@Remote async openSettings(permission: CuaSystemPermission, locale: string): Promise<void>
/**
* Remove saved and session access and join affected operations.
+6 -8
View File
@@ -14,7 +14,6 @@ CUA provider 通过 `ctx.cuaAccess` 提供系统权限状态、用户主动申
|---|---|
| `CuaApplicationId`、`CuaApplicationGrant` | 平台提供的应用身份,以及显示给用户的已保存决定。 |
| `CuaSystemPermission` | 用户选择申请的辅助功能或屏幕录制权限。 |
| `CuaPermissionGuideText` | Client 提供的本地化原生引导说明和恢复提示;应用名由 helper 提供。 |
| `CuaPermissionProcess`、`CuaSystemPermissionState` | 观测到的 helper、父进程和原生权限状态,以及 helper 启动时由 macOS 责任进程记录识别出的应用。系统归属信息不可用时,独立 helper 使用自身 bundle;两者都无法识别时才省略应用信息。 |
| `CuaApplicationState` | 持久授权、拒绝记录,以及能否持久保存。会话授权只保留在 provider 内部。系统权限和应用图标分别查询,页面在各项结果返回后分别显示。 |
@@ -104,21 +103,20 @@ Browser controls for provider-owned native permission and application decisions.
@Remote async applicationIcons(): Promise<Record<string, string>>
/**
* Open the selected System Settings pane; show the native drag guide only for a missing privilege.
* Guide preparation failures still attempt settings navigation before reporting the failure.
* Request the selected system privilege through the native provider.
* @param permission macOS privilege to request.
* @param text Localized guide copy; application identity comes from the native helper.
* @returns Current permission observations; opening the guide does not grant permission.
* @param locale Current Client language for provider-owned permission UI.
* @returns Current permission observations; requesting does not establish consent.
*/
@Remote async requestPermission(permission: CuaSystemPermission, text: CuaPermissionGuideText): Promise<CuaSystemPermissionState>
@Remote async requestPermission(permission: CuaSystemPermission, locale: string): Promise<CuaSystemPermissionState>
/**
* Open the macOS privacy pane selected by the user.
* @param permission Corresponding system privilege.
* @param failureMessage Localized manual navigation instructions if macOS rejects the open request.
* @param locale Current Client language for provider-owned recovery instructions.
* @returns Completion after macOS accepted the open request.
*/
@Remote async openSettings(permission: CuaSystemPermission, failureMessage: string): Promise<void>
@Remote async openSettings(permission: CuaSystemPermission, locale: string): Promise<void>
/**
* Remove saved and session access and join affected operations.
@@ -12,8 +12,8 @@
en: 4549f02fc53854a5
zh: 6f74f1130b6623a8
/deepseek-ai-dsh-experimental-cua-macos/table-of-contents:
en: ffab8081f3341b92
zh: 29867b195b3c7e51
en: 1c91307fdaebba7a
zh: 910fba0aa793f47f
/deepseek-ai-dsh-experimental-cua-macos/use-this-package:
en: ead96859cdf97de5
zh: acaf62c479d5d6e8
@@ -26,6 +26,8 @@ The helper runs through LaunchServices as the independent application `DSH Compu
Permission requests open the selected privacy pane and a native floating guide. The guide selects the largest settings window once and follows that window by ID. It uses the normal window level, ordered immediately above the selected window so authentication dialogs remain above it. Placement reads window bounds without requesting Accessibility access. The guide stays hidden until the settings window is identified; a failed settings-open request displays manual navigation instructions independently. A material alpha mask gives the panel and its shadow rounded corners. The latest request replaces the shared guide, and only its owning connection can close it during cleanup. Navigation is attempted even when the application is unavailable, and the page reports that failure. Already-granted permissions open settings without a guide; explicit settings navigation dismisses previous guidance. A rejected settings-open request reports `SETTINGS_UNAVAILABLE`; missing or replaced application targets report `APPLICATION_UNAVAILABLE`. System Settings owns drag acceptance and any relaunch requirement.
The provider resolves guide and recovery copy from the current Client language, with English for unsupported languages. Drag the guide’s application row into the permission list and enable its switch; enable the existing entry when already listed. The guide hides during dragging, closes when a drop is accepted, and returns after a canceled or rejected drag. The back button closes it and returns to the previous application. Right-click the row, or click it for keyboard focus and press Return or Space, to reveal the bundle in Finder. While open, the guide checks permission and window position every `permissionPollIntervalMs` (default 1000 ms), and closes after a confirmed grant, dismissal, disappearance of the settings window, or its owning connection’s cleanup. Dragging does not establish consent. Follow any macOS quit-and-reopen prompt; the shared helper is not automatically terminated.
-----
<a id="use-this-package"></a>
@@ -26,6 +26,8 @@ helper 通过 LaunchServices 作为独立应用 `DSH Computer Use.app` 运行。
权限请求打开对应隐私设置页和原生悬浮引导框。引导框首次选择面积最大的设置窗口,随后按窗口 ID 跟随。它使用普通窗口层级,紧邻所选窗口上方排列,使认证弹窗保持在其上方。定位读取窗口边界,不申请辅助功能权限。引导框等到识别出设置窗口后才显示;打开设置失败时独立显示手动导航说明。材质的透明度遮罩为面板和阴影提供圆角。最新请求替换共享引导,连接清理只关闭自身创建的引导。应用不可用时仍尝试打开设置,并在页面报告失败原因。权限已授予时只打开设置;显式进入设置会关闭原有引导。系统拒绝打开设置时报告 `SETTINGS_UNAVAILABLE`;应用目标缺失或被替换时报告 `APPLICATION_UNAVAILABLE`。系统设置决定是否接收拖入的应用,以及是否要求重新启动。
provider 按当前界面语言生成引导和恢复文案,不支持的语言使用英语。将引导框内的应用拖入权限列表并开启开关;已有此应用时直接开启现有条目。拖动时隐藏引导框,放置被接收后关闭,拖动取消或被拒绝后恢复。返回按钮关闭引导框并返回之前的应用。右键点击应用行,或点击它使引导框获得键盘焦点后按 Return、空格,可在 Finder 中选中同一应用。原生引导打开期间,每隔 `permissionPollIntervalMs` 检查权限和窗口位置,默认 1000 毫秒;在确认授权、用户关闭、系统设置窗口消失或所属连接清理时关闭。拖动应用不代表授权完成。若 macOS 提示退出并重新打开应用,请按提示操作;系统不会自动终止共享 helper。
-----
<a id="use-this-package"></a>
@@ -14,7 +14,7 @@ enum NativeProtocol {
"launchApp": ["appHandle"],
"listApps": [],
"listWindows": ["appHandle"],
"openPermissionSettings": ["permission"],
"openPermissionSettings": ["permission", "settingsMessage"],
"paste": ["appHandle", "format", "maxClipboardBytes", "maxDepth", "maxElements", "pasteReadTimeoutMs", "text"],
"performSecondaryAction": ["action", "appHandle", "elementIndex", "snapshotId"],
"permissionStatus": [],
@@ -41,7 +41,10 @@ import Foundation
"helper": Self.process(getpid()),
"parent": Self.process(getppid()),
]
if let launchApplication { result["launchApplication"] = launchApplication }
if let launchApplication, case .object(var application) = launchApplication {
application.removeValue(forKey: "bundleId")
result["launchApplication"] = .object(application)
}
return .object(result)
}
@@ -78,9 +81,13 @@ import Foundation
func closeGuide(owner: UUID) { guide.close(owner: owner) }
func openSettings(_ permission: String) throws(NativeFailure) -> JSONValue {
func openSettings(_ permission: String, failureMessage: String) throws(NativeFailure) -> JSONValue {
guide.dismiss()
return try openPane(permission)
do { return try openPane(permission) }
catch {
guard error.code == "SETTINGS_UNAVAILABLE" else { throw error }
throw NativeFailure(code: error.code, message: failureMessage)
}
}
private func openPane(_ permission: String) throws(NativeFailure) -> JSONValue {
@@ -184,7 +184,7 @@ import Darwin
result = try permissions.request(params.string("permission"), owner: permissionOwner, text: text,
interval: Double(params.integer("pollIntervalMs", in: 100...Int(Int32.max))) / 1000)
} else if request.method == "openPermissionSettings" {
result = try permissions.openSettings(params.string("permission"))
result = try permissions.openSettings(params.string("permission"), failureMessage: params.string("settingsMessage"))
} else if request.method == "findApps" {
result = .array(try ApplicationDiscovery.applications(matching: params.string("query"), exact: false,
limit: params.integer("maxAppMatches", in: 1...1000), cancellation: cancellation).map { $0.json(catalog: catalog) })
@@ -4,6 +4,8 @@ import { z } from 'zod'
import type { JSONSchema } from 'zod/v4/core'
import { NativeOnlyOperations, NativeResourceParameters, Request } from '@deepseek-ai/dsh-cua/protocol'
import { PermissionGuideParameters, PermissionSettingsParameters } from '../src/permission-guide.ts'
type Schema = JSONSchema.JSONSchema
function object(schema: Schema | boolean | undefined): Schema {
@@ -17,7 +19,8 @@ function variants(schema: Schema): Schema[] { return schema.anyOf ?? schema.oneO
* @returns Native JSON Schema and Swift field/enum tables, including private continuations.
*/
export function renderNativeProtocol(): { json: string; swift: string } {
const resources = new Map(Object.entries(NativeResourceParameters))
const resources = new Map<string, z.ZodObject>([...Object.entries(NativeResourceParameters),
['requestPermission', PermissionGuideParameters], ['openPermissionSettings', PermissionSettingsParameters]])
const modelMethods = Request.options.map(schema => schema.shape.method.value).sort()
const schemas = [...Request.options, ...NativeOnlyOperations].map(schema => z.toJSONSchema(schema))
const fields = new Map<string, Set<string>>()
@@ -1115,6 +1115,10 @@
"screenRecording"
]
},
"settingsMessage": {
"type": "string",
"minLength": 1
},
"instruction": {
"type": "string",
"minLength": 1
@@ -1131,10 +1135,6 @@
"type": "string",
"minLength": 1
},
"settingsMessage": {
"type": "string",
"minLength": 1
},
"pollIntervalMs": {
"type": "integer",
"minimum": 100,
@@ -1143,11 +1143,11 @@
},
"required": [
"permission",
"settingsMessage",
"instruction",
"backLabel",
"finderLabel",
"unavailableMessage",
"settingsMessage",
"pollIntervalMs"
],
"additionalProperties": false
@@ -1179,10 +1179,15 @@
"accessibility",
"screenRecording"
]
},
"settingsMessage": {
"type": "string",
"minLength": 1
}
},
"required": [
"permission"
"permission",
"settingsMessage"
],
"additionalProperties": false
}
@@ -43,6 +43,8 @@ func permissionRequestOpensTheNativeGuideAndMatchingSettings(permission: String)
#expect(!guide.isOpen)
let state = try permissions.request(permission, owner: owner, text: copy, interval: 1)
#expect(state[permission] == .bool(false))
#expect(state["launchApplication"]["name"] == .string("Helper.app"))
#expect(state["launchApplication"]["bundleId"] == .null)
#expect(guide.isOpen)
#expect(guide.application?.url.path == "/generation/Helper.app")
#expect(guide.text?.instruction == copy.instruction)
@@ -123,7 +125,7 @@ func unavailablePermissionApplicationsStillOpenSettings(identified: Bool) throws
guide.isOpen = true
let permissions = SystemPermissions(application: application,
actions: SystemPermissionActions(accessibility: { false }, screenRecording: { true }, open: { _ in true }), guide: guide)
#expect(try permissions.openSettings("screenRecording")["opened"] == .bool(true))
#expect(try permissions.openSettings("screenRecording", failureMessage: copy.settingsMessage)["opened"] == .bool(true))
#expect(!guide.isOpen)
}
@@ -223,3 +225,21 @@ func permissionGuideMaterialKeepsTransparentCornersWhenResized(size: NSSize) thr
#expect(PermissionGuidePlacement.needsOrdering(guide: 20, above: 10, frontToBack: [30, 10]))
#expect(!PermissionGuidePlacement.needsOrdering(guide: 20, above: 10, frontToBack: [30, 20, 10]))
}
@MainActor @Test(arguments: ["accessibility", "invalid"])
func explicitSettingsFailuresPreserveValidationAndLocalizeNavigation(permission: String) throws {
let permissions = SystemPermissions(application: application,
actions: SystemPermissionActions(accessibility: { false }, screenRecording: { false }, open: { _ in false }),
guide: GuideFixture())
do {
_ = try permissions.openSettings(permission, failureMessage: "请手动打开系统设置中的辅助功能")
Issue.record("Failed settings navigation was accepted")
} catch let failure as NativeFailure {
if permission == "accessibility" {
#expect(failure.code == "SETTINGS_UNAVAILABLE")
#expect(failure.message == "请手动打开系统设置中的辅助功能")
} else {
#expect(failure.code == "INVALID_ARGUMENT")
}
}
}
+2 -1
View File
@@ -55,7 +55,8 @@
"dependencies": {
"zod": "^4.4.3",
"@deepseek-ai/dsh-atomic-write": "workspace:*",
"@deepseek-ai/dsh-cua": "workspace:*"
"@deepseek-ai/dsh-cua": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/dsh-working-directory": "workspace:*",
+14 -5
View File
@@ -1,6 +1,8 @@
/** macOS native CUA provider using the shared application and window API. */
import { isAbsolute, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import Schema from '@deepseek-ai/schemastery'
import { createPermissionParameters } from './permission-guide.ts'
import type { Context } from '@deepseek-ai/cordis'
import { CuaConfig, installCua } from '@deepseek-ai/dsh-cua'
import { nativeHostArchitecture, prepareNativeExecutable } from './native-build.ts'
@@ -13,9 +15,14 @@ export const name = 'experimental-cua-macos'
/** Services required by the shared CUA executor and the native launcher. */
export const inject = ['computerUse', 'tools', 'systemPrompt', 'subprocess', 'agents']
/** Native preparation, operation bounds, and profile-local application grants. */
export type Config = CuaConfig
/** Validate the shared configuration before preparing the macOS helper. */
export const Config: typeof CuaConfig = CuaConfig
export interface Config extends CuaConfig {
/** Interval in milliseconds between permission and position checks while the native guide is open. */
permissionPollIntervalMs: number
}
/** Validate shared resource bounds and macOS guide configuration before preparing the helper. */
export const Config = Schema.intersect([CuaConfig, Schema.object({
permissionPollIntervalMs: Schema.number().step(1).min(100).max(2 ** 31 - 1).default(1000),
})]) satisfies Schema<Record<string, unknown>, Config>
const description = 'Inspect macOS permissions, select an application and window, read AX state, capture screenshots or dispatch input. Use appHandle from getApp; verify actions by reading fresh state.'
@@ -26,9 +33,11 @@ const description = 'Inspect macOS permissions, select an application and window
* @returns Completion after native preparation and tool registration.
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
const backend = { createChannel: createSharedNativeChannel, guidance: GUIDANCE, description,
permissionParameters: createPermissionParameters(config.permissionPollIntervalMs) }
if (config.helperPath !== undefined) {
if (!isAbsolute(config.helperPath)) throw new Error('dsh-cua helperPath must be absolute')
installCua(ctx, config, { helperPath: config.helperPath, createChannel: createSharedNativeChannel, guidance: GUIDANCE, description })
installCua(ctx, config, { helperPath: config.helperPath, ...backend })
return
}
const architecture = nativeHostArchitecture(process.platform, process.arch)
@@ -45,5 +54,5 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
}, 'dsh-cua.native-preparation')
const helperPath = await prepared
controller.signal.throwIfAborted()
installCua(ctx, config, { helperPath, createChannel: createSharedNativeChannel, guidance: GUIDANCE, description })
installCua(ctx, config, { helperPath, ...backend })
}
@@ -0,0 +1,110 @@
/** macOS permission guide copy and private native request parameters. */
import { z } from 'zod'
import type { CuaBackend } from '@deepseek-ai/dsh-cua'
/** Native-only fields for opening the selected privacy pane. */
export const PermissionSettingsParameters = z.object({ settingsMessage: z.string().min(1) })
/** Native-only guide fields, validated again by the Swift request parser. */
export const PermissionGuideParameters = PermissionSettingsParameters.extend({
instruction: z.string().min(1), backLabel: z.string().min(1), finderLabel: z.string().min(1),
unavailableMessage: z.string().min(1), pollIntervalMs: z.number().int().min(100).max(2 ** 31 - 1),
})
const en = {
instruction: 'Drag {application} to the list above to allow {permission}',
backLabel: 'Back',
finderLabel: 'Show in Finder',
unavailableMessage: 'Application unavailable. Enable Computer Use again and retry.',
settingsMessage: 'Open System Settings > Privacy & Security > {permission} manually.',
accessibility: 'Accessibility',
screenRecording: 'Screen Recording',
}
const zh = {
instruction: '将 {application} 拖入上方列表以允许{permission}',
backLabel: '返回',
finderLabel: '在 Finder 中显示',
unavailableMessage: '找不到此应用。请重新启用 Computer Use 后重试',
settingsMessage: '请手动打开系统设置 > 隐私与安全性 > {permission}',
accessibility: '辅助功能',
screenRecording: '屏幕录制',
} satisfies typeof en
const ja = {
instruction: '{permission}を許可するには、{application}を上の一覧にドラッグしてください',
backLabel: '戻る',
finderLabel: 'Finder で表示',
unavailableMessage: 'アプリが見つかりません。Computer Use を再度有効にしてください。',
settingsMessage: 'システム設定 > プライバシーとセキュリティ > {permission} を開いてください。',
accessibility: 'アクセシビリティ',
screenRecording: '画面収録',
} satisfies typeof en
const es = {
instruction: 'Arrastra {application} a la lista de arriba para permitir {permission}',
backLabel: 'Volver',
finderLabel: 'Mostrar en Finder',
unavailableMessage: 'Aplicación no disponible. Vuelve a activar Computer Use e inténtalo de nuevo.',
settingsMessage: 'Abre Configuración del sistema > Privacidad y seguridad > {permission}.',
accessibility: 'Accesibilidad',
screenRecording: 'Grabación de pantalla',
} satisfies typeof en
const fr = {
instruction: 'Glissez {application} dans la liste ci-dessus pour autoriser {permission}',
backLabel: 'Retour',
finderLabel: 'Afficher dans le Finder',
unavailableMessage: 'Application introuvable. Réactivez Computer Use et réessayez.',
settingsMessage: 'Ouvrez Réglages Système > Confidentialité et sécurité > {permission}.',
accessibility: 'Accessibilité',
screenRecording: "Enregistrement d'écran",
} satisfies typeof en
const de = {
instruction: 'Ziehe {application} in die Liste oben, um {permission} zu erlauben',
backLabel: 'Zurück',
finderLabel: 'Im Finder anzeigen',
unavailableMessage: 'Anwendung nicht verfügbar. Aktiviere Computer Use erneut.',
settingsMessage: 'Öffne Systemeinstellungen > Datenschutz & Sicherheit > {permission}.',
accessibility: 'Barrierefreiheit',
screenRecording: 'Bildschirmaufzeichnung',
} satisfies typeof en
const ru = {
instruction: 'Перетащите {application} в список выше, чтобы разрешить {permission}',
backLabel: 'Назад',
finderLabel: 'Показать в Finder',
unavailableMessage: 'Приложение недоступно. Снова включите Computer Use и повторите попытку.',
settingsMessage: 'Откройте Системные настройки > Конфиденциальность и безопасность > {permission}.',
accessibility: 'Доступность',
screenRecording: 'Запись экрана',
} satisfies typeof en
const pt = {
instruction: 'Arraste {application} para a lista acima para permitir {permission}',
backLabel: 'Voltar',
finderLabel: 'Mostrar no Finder',
unavailableMessage: 'Aplicativo indisponível. Ative Computer Use novamente e tente de novo.',
settingsMessage: 'Abra Ajustes do Sistema > Privacidade e Segurança > {permission}.',
accessibility: 'Acessibilidade',
screenRecording: 'Gravação de tela',
} satisfies typeof en
const dictionaries = new Map<string, typeof en>(Object.entries({ en, zh, ja, es, fr, de, ru, pt }))
/**
* Resolve native permission copy from the Client language; unsupported languages use English.
* @param pollIntervalMs Interval between native guide permission and position checks.
* @returns Provider-owned parameters for the selected permission action.
*/
export function createPermissionParameters(pollIntervalMs: number): CuaBackend['permissionParameters'] {
return (method, permission, locale) => {
const language = locale.toLowerCase().replace(/-.*/, '')
const text = dictionaries.get(language) ?? en
const settingsMessage = text.settingsMessage.replace('{permission}', text[permission])
if (method === 'openPermissionSettings') return { permission, settingsMessage }
return { permission, settingsMessage, pollIntervalMs,
instruction: text.instruction.replace('{permission}', text[permission]),
backLabel: text.backLabel, finderLabel: text.finderLabel, unavailableMessage: text.unavailableMessage }
}
}
@@ -0,0 +1,38 @@
/** Native permission preparation belongs to the macOS provider. */
import { expect, it } from 'vitest'
import { CuaConfig } from '@deepseek-ai/dsh-cua'
import { Config } from '../src/index.ts'
import { createPermissionParameters, PermissionGuideParameters, PermissionSettingsParameters } from '../src/permission-guide.ts'
it('keeps the guide interval in macOS configuration and rejects invalid bounds', () => {
expect(CuaConfig({})).not.toHaveProperty('permissionPollIntervalMs')
expect(Config({}).permissionPollIntervalMs).toBe(1000)
expect(Config({ permissionPollIntervalMs: 250 }).permissionPollIntervalMs).toBe(250)
for (const permissionPollIntervalMs of [99, 100.5, 2 ** 31]) expect(() => Config({ permissionPollIntervalMs })).toThrow()
})
it.each(['en', 'zh', 'ja', 'es', 'fr', 'de', 'ru', 'pt'])('prepares both permissions in %s without sending settings-only callers guide fields', (locale) => {
const parameters = createPermissionParameters(250)
for (const permission of ['accessibility', 'screenRecording'] as const) {
const guide = parameters('requestPermission', permission, locale)
expect(PermissionGuideParameters.parse(guide)).toMatchObject({ pollIntervalMs: 250 })
expect(guide.instruction).toContain('{application}')
expect(guide.instruction).not.toContain('{permission}')
const settings = parameters('openPermissionSettings', permission, locale)
expect(settings).toEqual({ permission, settingsMessage: guide.settingsMessage })
expect(PermissionSettingsParameters.parse(settings)).toEqual({ settingsMessage: guide.settingsMessage })
}
})
it('matches language variants and falls back to English for unsupported languages', () => {
const parameters = createPermissionParameters(2000)
expect(parameters('requestPermission', 'accessibility', 'ZH-CN')).toEqual({
permission: 'accessibility', pollIntervalMs: 2000,
instruction: '将 {application} 拖入上方列表以允许辅助功能', backLabel: '返回', finderLabel: '在 Finder 中显示',
unavailableMessage: '找不到此应用。请重新启用 Computer Use 后重试',
settingsMessage: '请手动打开系统设置 > 隐私与安全性 > 辅助功能',
})
expect(parameters('requestPermission', 'screenRecording', 'it')).toEqual(parameters('requestPermission', 'screenRecording', 'en'))
expect(() => PermissionGuideParameters.parse({ ...parameters('requestPermission', 'accessibility', 'en'), pollIntervalMs: 0 })).toThrow()
expect(() => PermissionSettingsParameters.parse({ settingsMessage: '' })).toThrow()
})
@@ -754,3 +754,20 @@ it.each([true, false])('uses configured grants without requesting a human answer
expect(native.instances).toHaveLength(1)
expect(native.request).toHaveBeenCalledTimes(2)
})
it('routes permission actions through the macOS provider with the Client language and configured interval', async () => {
const f = await setup()
native.request.mockResolvedValueOnce({ accessibility: false, screenRecording: false, helper: { pid: 9 }, parent: { pid: 8 } })
await f.ctx.cuaAccess.requestPermission('accessibility', 'zh')
expect(native.request).toHaveBeenLastCalledWith('requestPermission', {
permission: 'accessibility', pollIntervalMs: 1000,
instruction: '将 {application} 拖入上方列表以允许辅助功能', backLabel: '返回', finderLabel: '在 Finder 中显示',
unavailableMessage: '找不到此应用。请重新启用 Computer Use 后重试',
settingsMessage: '请手动打开系统设置 > 隐私与安全性 > 辅助功能',
}, expect.any(AbortSignal))
native.request.mockResolvedValueOnce({ opened: true })
await f.ctx.cuaAccess.openSettings('screenRecording', 'en')
expect(native.request).toHaveBeenLastCalledWith('openPermissionSettings', {
permission: 'screenRecording', settingsMessage: 'Open System Settings > Privacy & Security > Screen Recording manually.',
}, expect.any(AbortSignal))
})
@@ -19,6 +19,9 @@
},
{
"path": "../../util/atomic-write"
},
{
"path": "../../../vendor/schemastery"
}
]
}
+2 -2
View File
@@ -18,8 +18,8 @@
en: ed736a45140a6d56
zh: 8dc89373a0a88fcc
/deepseek-ai-dsh-cua/use-this-package/permission-controls:
en: 7c64adec7de815cf
zh: f9b0c7fbeb5eb7c3
en: 2da530aaad735728
zh: 9fe12c2febbfcb58
/deepseek-ai-dsh-cua/understand-the-implementation:
en: 90e8fcb6ebe271d1
zh: f0c90834bec8c73a
+2 -2
View File
@@ -45,9 +45,9 @@ Actions return `dispatched:true, verified:false`; read fresh state to verify the
On the Computer Use detail page, permission cards, component rows, and source facts use l3 borders, with 32px gaps between sections. Application badges retain their l1 borders.
The [Computer Use bundle](../cua-bundle/README.md) mounts this page with its native provider. **Authorize computer use** groups the new-session default, PTC execution mode, Accessibility, and Screen Recording inside one border. Each permission row shows the responsible application’s name and icon. **Request permission** opens the selected System Settings pane; the native guide appears over its lower margin once the window is identified. Drag the application row from this guide into the permission list, then enable its switch; enable the existing entry when already listed. The guide hides during dragging, closes when a drop is accepted, and returns after a canceled or rejected drag. The back button closes the guide and returns to the previous application. Right-click the application row, or click it to give the guide keyboard focus and press Return or Space, to reveal the same bundle in Finder. An outlined **Allowed** button opens settings for revocation; unknown status uses **Unknown**. Application grants remain in a separate group.
The [Computer Use bundle](../cua-bundle/README.md) mounts this page with its native provider. **Authorize computer use** groups the new-session default, PTC execution mode, Accessibility, and Screen Recording inside one border. Each permission row shows the responsible application’s name and icon. **Request permission** delegates to the provider with the selected permission and current Client language; the [macOS provider](../cua-macos/README.md) owns the native drag guide. An outlined **Allowed** button opens settings for revocation; unknown status uses **Unknown**. Application grants remain in a separate group.
The page refreshes on entry, window focus, and completed actions. Query failures retain the last observation as unknown and offer **Retry**. While open, the native guide checks permission and window position every `permissionPollIntervalMs` (default 1000 ms), and closes after a confirmed grant, dismissal, disappearance of the settings window, or its owning connection’s cleanup. Opening settings or dragging the application does not establish consent. Settings-open failures retain manual navigation instructions. Follow any macOS quit-and-reopen prompt; the shared helper is not automatically terminated.
The page refreshes on entry, window focus, and completed actions. Query failures retain the last observation as unknown and offer **Retry**. Opening settings or requesting permission does not establish consent. The provider owns native guidance, its configuration, and localized recovery instructions.
System-permission operations affect the computer running DSH. When accessing DSH remotely, complete authorization on that computer.
+2 -2
View File
@@ -45,9 +45,9 @@ kind: "package-reference"
Computer Use 详情页的权限卡片、组件列表和来源信息使用 l3 边框,区块间距为 32px。应用名称徽标保留 l1 边框。
[Computer Use 组合](../cua-bundle/README.zh.md)将本页与原生 provider 一起挂载。“给 Computer Use 授权”把新会话默认模式、PTC 执行方式、辅助功能和屏幕录制放在同一个边框内。每条权限要求显示责任应用的名称和图标。“申请权限”打开对应系统设置页;识别到设置窗口后,原生引导框才在其底部显示。将引导框内的应用拖入权限列表,再开启开关;已有此应用时直接开启现有条目。拖动时隐藏引导框,放置被接收后关闭,拖动取消或被拒绝后恢复。返回按钮关闭引导框并返回之前的应用。右键点击应用行,或点击它使引导框获得键盘焦点后按 Return、空格,可在 Finder 中选中同一应用。“已允许”描边按钮打开设置以撤销权限;未知状态使用“未确认”。应用访问授权保持独立分组。
[Computer Use 组合](../cua-bundle/README.zh.md)将本页与原生 provider 一起挂载。“给 Computer Use 授权”把新会话默认模式、PTC 执行方式、辅助功能和屏幕录制放在同一个边框内。每条权限要求显示责任应用的名称和图标。“申请权限”将所选权限和当前界面语言交给 provider;原生拖动引导由 [macOS provider](../cua-macos/README.zh.md)负责。“已允许”描边按钮打开设置以撤销权限;未知状态使用“未确认”。应用访问授权保持独立分组。
进入页面、返回窗口和操作结束后会刷新状态。查询失败保留上次观测但标为未确认,并提供“重试”。原生引导打开期间,每隔 `permissionPollIntervalMs` 检查权限和窗口位置,默认 1000 毫秒;在确认授权、用户关闭、系统设置窗口消失或所属连接清理时关闭。打开设置或拖动应用不代表授权完成。设置打开失败时保留手动导航说明。若 macOS 提示退出并重新打开应用,请按提示操作;系统不会自动终止共享 helper。
进入页面、返回窗口和操作结束后会刷新状态。查询失败保留上次观测但标为未确认,并提供“重试”。打开设置或申请权限不代表授权完成。原生引导、配置和本地化恢复提示由 provider 负责。
系统权限操作作用于运行 DSH 的电脑。远程访问时,请在该电脑上完成授权。
@@ -4,14 +4,6 @@ export type { CuaApplicationGrant, CuaApplicationId } from './application-types.
/** macOS privileges used by the native provider. */
export type CuaSystemPermission = 'accessibility' | 'screenRecording'
/** Localized native guide copy; the helper replaces the literal {application} with its own bundle name. */
export interface CuaPermissionGuideText {
instruction: string
backLabel: string
finderLabel: string
unavailableMessage: string
settingsMessage: string
}
/** Observed process; neither the helper nor its parent is assumed to be the TCC owner. */
export interface CuaPermissionProcess {
pid: number
@@ -25,7 +17,7 @@ export interface CuaSystemPermissionState {
helper: CuaPermissionProcess
parent: CuaPermissionProcess
/** Application macOS reports as responsible for the helper, captured at startup; absent when it cannot be identified. */
launchApplication?: { name: string; path: string; icon?: string | undefined; bundleId?: string | undefined } | undefined
launchApplication?: { name: string; path: string; icon?: string | undefined } | undefined
}
/** Saved grants and refused applications, independent of native permission checks and icons. */
export interface CuaApplicationState {
+16 -23
View File
@@ -8,22 +8,21 @@ import { ApplicationAccess, type AccessOptions } from './application-access.ts'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import { z } from 'zod'
import { ApplicationIcon } from './protocol.ts'
import { NativeClient, NativeError, type NativeClientConfig, type NativeChannelFactory } from './native-client.ts'
import type { CuaApplicationId, CuaApplicationState, CuaPermissionGuideText, CuaSystemPermission, CuaSystemPermissionState } from './access-types.ts'
import { NativeClient, type NativeClientConfig, type NativeChannelFactory } from './native-client.ts'
import type { CuaApplicationId, CuaApplicationState, CuaSystemPermission, CuaSystemPermissionState } from './access-types.ts'
export { ApplicationAuthorizationResult } from './application-access.ts'
export type * from './access-types.ts'
interface ControlOptions extends AccessOptions {
native: NativeClientConfig
createChannel: NativeChannelFactory
permissionPollIntervalMs: number
permissionParameters: (method: 'requestPermission' | 'openPermissionSettings', permission: CuaSystemPermission, locale: string) => Record<string, JsonValue>
}
type Control = (method: string, params: Record<string, JsonValue>) => Promise<JsonValue>
const permissionProcess = z.object({ pid: z.number().int().positive(), path: z.string().optional(), name: z.string().optional() })
const systemPermissions = z.object({
accessibility: z.boolean(), screenRecording: z.boolean(), helper: permissionProcess, parent: permissionProcess,
launchApplication: z.object({ name: z.string().min(1), path: z.string().min(1),
icon: ApplicationIcon.optional(), bundleId: z.string().optional() }).optional(),
launchApplication: z.object({ name: z.string().min(1), path: z.string().min(1), icon: ApplicationIcon.optional() }).optional(),
})
declare module '@deepseek-ai/cordis' {
@@ -39,10 +38,10 @@ export class CuaAccess extends TypertRemoteService {
* @param ctx Provider context.
* @param policy Application decision owner.
* @param control Serialized native permission dispatch.
* @param permissionPollIntervalMs Interval between native guide permission and position checks.
* @param permissionParameters Provider-owned native parameters for the selected action and Client language.
*/
constructor(ctx: Context, private readonly policy: ApplicationAccess,
private readonly control: Control, private readonly permissionPollIntervalMs: number) {
private readonly control: Control, private readonly permissionParameters: ControlOptions['permissionParameters']) {
super(ctx, 'cuaAccess')
}
@@ -99,33 +98,27 @@ export class CuaAccess extends TypertRemoteService {
}
/**
* Open the selected System Settings pane; show the native drag guide only for a missing privilege.
* Guide preparation failures still attempt settings navigation before reporting the failure.
* Request the selected system privilege through the native provider.
* @param permission macOS privilege to request.
* @param text Localized guide copy; application identity comes from the native helper.
* @returns Current permission observations; opening the guide does not grant permission.
* @param locale Current Client language for provider-owned permission UI.
* @returns Current permission observations; requesting does not establish consent.
*/
@Remote
async requestPermission(permission: CuaSystemPermission, text: CuaPermissionGuideText): Promise<CuaSystemPermissionState> {
async requestPermission(permission: CuaSystemPermission, locale: string): Promise<CuaSystemPermissionState> {
return systemPermissions.parse(await this.control('requestPermission',
{ permission, ...text, pollIntervalMs: this.permissionPollIntervalMs }))
this.permissionParameters('requestPermission', permission, locale)))
}
/**
* Open the macOS privacy pane selected by the user.
* @param permission Corresponding system privilege.
* @param failureMessage Localized manual navigation instructions if macOS rejects the open request.
* @param locale Current Client language for provider-owned recovery instructions.
* @returns Completion after macOS accepted the open request.
*/
@Remote
async openSettings(permission: CuaSystemPermission, failureMessage: string): Promise<void> {
try { z.object({ opened: z.literal(true) }).parse(await this.control('openPermissionSettings', { permission })) }
catch (error) {
if (error instanceof NativeError && error.code === 'SETTINGS_UNAVAILABLE') {
throw new NativeError(error.code, failureMessage, { cause: error })
}
throw error
}
async openSettings(permission: CuaSystemPermission, locale: string): Promise<void> {
z.object({ opened: z.literal(true) }).parse(await this.control('openPermissionSettings',
this.permissionParameters('openPermissionSettings', permission, locale)))
}
/**
@@ -174,6 +167,6 @@ export function installCuaAccess(ctx: Context, options: ControlOptions): Pick<Ap
await Promise.allSettled([policy.dispose(), tail])
await client?.dispose()
}, 'cua-access.dispose')
new CuaAccess(ctx, policy, control, options.permissionPollIntervalMs)
new CuaAccess(ctx, policy, control, options.permissionParameters)
return { select: policy.select.bind(policy), run: policy.run.bind(policy), forget: policy.forget.bind(policy), dispose }
}
@@ -4,7 +4,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type {} from '@deepseek-ai/dsh-cua/remote'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { CuaApplicationId, CuaApplicationState, CuaPermissionGuideText, CuaSystemPermission, CuaSystemPermissionState } from '../access-types.ts'
import type { CuaApplicationId, CuaApplicationState, CuaSystemPermission, CuaSystemPermissionState } from '../access-types.ts'
interface PermissionData {
runtime: { persistent: boolean; writable: boolean }
@@ -52,10 +52,10 @@ export class PermissionController {
private sequence = 0
/**
* @param remote Generated CUA Remote namespace; failed results follow the query and action paths.
* @param guideText Copy in the current Client locale for the selected native permission guide.
* @param locale Current Client language, read when a permission action starts.
*/
constructor(private readonly remote: Context['remote']['cuaAccess'],
private readonly guideText: (permission: CuaSystemPermission) => CuaPermissionGuideText) {}
private readonly locale: () => string) {}
/**
* Bind the permission projection to its user actions.
* @returns Reactive state and controls shared by the page, prompt, and toast host.
@@ -65,10 +65,10 @@ export class PermissionController {
hooks: { cuaPermissions: this.state },
refresh: () => { this.enqueue() },
request: (permission) => {
this.enqueue(() => this.remote.requestPermission(permission, this.guideText(permission)), 'requested')
this.enqueue(() => this.remote.requestPermission(permission, this.locale()), 'requested')
},
openSettings: (permission) => {
this.enqueue(() => this.remote.openSettings(permission, this.guideText(permission).settingsMessage), 'opened')
this.enqueue(() => this.remote.openSettings(permission, this.locale()), 'opened')
},
revoke: (id) => { this.enqueue(() => this.remote.revoke(id), 'revoked') },
retryApproval: (id) => { this.enqueue(() => this.remote.retryApproval(id), 'retryAllowed') },
@@ -2,11 +2,6 @@
export const NS = 'cuaPermissions'
/** English permission-page dictionary. */
export const en = {
nativeInstruction: 'Drag {application} to the list above to allow {permission}',
nativeBack: 'Back',
nativeFinder: 'Show in Finder',
nativeUnavailable: 'Application unavailable. Enable Computer Use again and retry.',
nativeSettingsUnavailable: 'Open System Settings > Privacy & Security > {permission} manually.',
title: 'Authorize computer use',
applicationsTitle: 'Manage applications computer use can access',
setupTitle: 'Set up computer use before use',
@@ -45,11 +40,6 @@ export const en = {
export type CuaPermissionLocaleKey = keyof typeof en
/** Chinese permission-page dictionary. */
export const zh: Record<CuaPermissionLocaleKey, string> = {
nativeInstruction: '将 {application} 拖入上方列表以允许{permission}',
nativeBack: '返回',
nativeFinder: '在 Finder 中显示',
nativeUnavailable: '找不到此应用。请重新启用 Computer Use 后重试',
nativeSettingsUnavailable: '请手动打开系统设置 > 隐私与安全性 > {permission}',
title: '给 Computer Use 授权', applicationsTitle: '管理 Computer Use 可访问的应用', setupTitle: '使用 Computer Use 前需要设置',
setupBody: 'Computer Use 插件已开启。使用前请切换到 PTC 模式,并授予辅助功能和屏幕录制权限。请前往插件详情页完成设置。',
setupQueryFailed: 'Computer Use 插件已开启,但暂时无法检测系统权限。请前往插件详情页查看原因并重试。',
@@ -85,11 +75,6 @@ export const zh: Record<CuaPermissionLocaleKey, string> = {
/** Japanese copy, checked against the English keys. */
export const ja = {
nativeInstruction: '{permission}を許可するには、{application}を上の一覧にドラッグしてください',
nativeBack: '戻る',
nativeFinder: 'Finder で表示',
nativeUnavailable: 'アプリが見つかりません。Computer Use を再度有効にしてください。',
nativeSettingsUnavailable: 'システム設定 > プライバシーとセキュリティ > {permission} を開いてください。',
'title': 'コンピュータ操作を承認',
'applicationsTitle': 'Computer Useがアクセスできるアプリを管理',
'setupTitle': '使用前にComputer Useを設定',
@@ -143,11 +128,6 @@ export const ja = {
/** Spanish dictionary, checked against the English keys. */
export const es = {
nativeInstruction: 'Arrastra {application} a la lista de arriba para permitir {permission}',
nativeBack: 'Volver',
nativeFinder: 'Mostrar en Finder',
nativeUnavailable: 'Aplicación no disponible. Vuelve a activar Computer Use e inténtalo de nuevo.',
nativeSettingsUnavailable: 'Abre Configuración del sistema > Privacidad y seguridad > {permission}.',
title: 'Autorizar el uso de la computadora',
applicationsTitle: 'Administrar aplicaciones a las que puede acceder el uso de la computadora',
setupTitle: 'Configurar el uso de la computadora antes de usarla',
@@ -185,11 +165,6 @@ export const es = {
/** French dictionary, checked against the English keys. */
export const fr = {
nativeInstruction: 'Glissez {application} dans la liste ci-dessus pour autoriser {permission}',
nativeBack: 'Retour',
nativeFinder: 'Afficher dans le Finder',
nativeUnavailable: 'Application introuvable. Réactivez Computer Use et réessayez.',
nativeSettingsUnavailable: 'Ouvrez Réglages Système > Confidentialité et sécurité > {permission}.',
title: "Autoriser l'utilisation de l'ordinateur",
applicationsTitle: "Gérer les applications auxquelles l'ordinateur peut accéder",
setupTitle: "Configurer l'utilisation de l'ordinateur avant utilisation",
@@ -227,11 +202,6 @@ export const fr = {
/** German dictionary, checked against the English keys. */
export const de = {
nativeInstruction: 'Ziehe {application} in die Liste oben, um {permission} zu erlauben',
nativeBack: 'Zurück',
nativeFinder: 'Im Finder anzeigen',
nativeUnavailable: 'Anwendung nicht verfügbar. Aktiviere Computer Use erneut.',
nativeSettingsUnavailable: 'Öffne Systemeinstellungen > Datenschutz & Sicherheit > {permission}.',
title: 'Autorisieren Sie die Computernutzung',
applicationsTitle: 'Verwalten Sie Anwendungen, auf die der Computer zugreifen kann',
setupTitle: 'Richten Sie die Computernutzung vor der Nutzung ein',
@@ -269,11 +239,6 @@ export const de = {
/** Russian dictionary, checked against the English keys. */
export const ru = {
nativeInstruction: 'Перетащите {application} в список выше, чтобы разрешить {permission}',
nativeBack: 'Назад',
nativeFinder: 'Показать в Finder',
nativeUnavailable: 'Приложение недоступно. Снова включите Computer Use и повторите попытку.',
nativeSettingsUnavailable: 'Откройте Системные настройки > Конфиденциальность и безопасность > {permission}.',
title: 'Разрешить использование компьютера',
applicationsTitle: 'Управление приложениями, к которым может иметь доступ компьютер',
setupTitle: 'Настройте использование компьютера перед использованием',
@@ -311,11 +276,6 @@ export const ru = {
/** Portuguese dictionary, checked against the English keys. */
export const pt = {
nativeInstruction: 'Arraste {application} para a lista acima para permitir {permission}',
nativeBack: 'Voltar',
nativeFinder: 'Mostrar no Finder',
nativeUnavailable: 'Aplicativo indisponível. Ative Computer Use novamente e tente de novo.',
nativeSettingsUnavailable: 'Abra Ajustes do Sistema > Privacidade e Segurança > {permission}.',
title: 'Autorizar o uso do computador',
applicationsTitle: 'Gerenciar aplicativos que o computador pode acessar',
setupTitle: 'Configure o uso do computador antes de usar',
@@ -34,13 +34,7 @@ export async function mountCuaPermissions(ctx: Context, contribution: TypertRemo
const transport = ctx.plugin(async (owner) => { await owner.remote.$mount(contribution) })
const ui = ctx.inject(['remote.cuaAccess', 'slots', 'locale'], (ctx) => {
ctx.effect(() => ctx.locale.register(NS, { en, zh, ja, es, fr, de, ru, pt }))
const t = ctx.locale.bind(NS)
const controller = new PermissionController(ctx.remote.cuaAccess, permission => ({
instruction: t('nativeInstruction', { application: '{application}', permission: t(permission) }),
backLabel: t('nativeBack'), finderLabel: t('nativeFinder'),
unavailableMessage: t('nativeUnavailable'),
settingsMessage: t('nativeSettingsUnavailable', { permission: t(permission) }),
}))
const controller = new PermissionController(ctx.remote.cuaAccess, () => ctx.locale.getSnapshot().active)
const face = controller.face()
ctx.slots.inject('conversation.composer', () => ctx.slots.register({
name: 'conversation.composer', priority: -1, locale: NS,
+10 -5
View File
@@ -15,7 +15,7 @@ import { z } from 'zod'
import { NativeClient, NativeError, type NativeClientConfig, type NativeChannelFactory } from './native-client.ts'
import { CuaPermissionStore } from './permission-store.ts'
import { ApplicationAuthorizationResult, installCuaAccess } from './access.ts'
import type { CuaApplicationGrant, CuaApplicationId } from './access-types.ts'
import type { CuaApplicationGrant, CuaApplicationId, CuaSystemPermission } from './access-types.ts'
import type { CuaAppHandle, CuaOperation } from './types.ts'
import { ApplicationIcon, AppResult, NativeScreenshotResult, Request, ScreenshotResult, StateResult, WindowsResult } from './protocol.ts'
import { CuaCoordinates } from './coordinates.ts'
@@ -71,8 +71,6 @@ export interface CuaConfig {
textEventIntervalMs: number
/** Native connection cleanup, socket-write and launcher termination budget in milliseconds. */
graceMs: number
/** Interval in milliseconds between permission and position checks while the native guide is open. */
permissionPollIntervalMs: number
/** Maximum nodes admitted by a native accessibility snapshot. */
maxElements: number
/** Maximum depth traversed by a native accessibility snapshot. */
@@ -102,7 +100,6 @@ export const CuaConfig = Schema.object({
maxResponseBytes: Schema.number().step(1).min(1024).default(32 * 1024 * 1024),
textEventIntervalMs: Schema.number().min(0).max(1000).default(0.25),
graceMs: Schema.number().step(1).min(1).max(2 ** 31 - 1).default(3000),
permissionPollIntervalMs: Schema.number().step(1).min(100).max(2 ** 31 - 1).default(1000),
maxElements: Schema.number().step(1).min(1).max(10000).default(2000),
maxDepth: Schema.number().step(1).min(1).max(100).default(25),
}) satisfies Schema<Record<string, unknown>, CuaConfig>
@@ -151,6 +148,14 @@ export interface CuaBackend {
createChannel: NativeChannelFactory
guidance: string
description: string
/**
* Build native permission parameters for the selected action and Client language.
* @param method Native permission action.
* @param permission System privilege selected by the user.
* @param locale Current Client language.
* @returns Parameters accepted by the provider's native implementation.
*/
permissionParameters: (method: 'requestPermission' | 'openPermissionSettings', permission: CuaSystemPermission, locale: string) => Record<string, JsonValue>
}
/**
@@ -170,7 +175,7 @@ export function installCua(ctx: Context, config: CuaConfig, backend: CuaBackend)
}
const access = installCuaAccess(ctx, {
native, createChannel: backend.createChannel,
permissionPollIntervalMs: config.permissionPollIntervalMs,
permissionParameters: backend.permissionParameters,
store: new CuaPermissionStore(ctx, config.allowedApps.map(grant => ({ ...grant, id: grant.id as CuaApplicationId })), config.timeoutMs),
})
const clients = new Map<Agent, NativeClient>()
+1 -4
View File
@@ -85,10 +85,7 @@ export const NativeResourceParameters = {
export const NativeOnlyOperations = [
operation('launchApp', app), operation('permissionStatus', {}),
operation('applicationIcons', { paths: z.array(z.string().min(1)) }),
operation('requestPermission', { permission: z.enum(['accessibility', 'screenRecording']),
instruction: z.string().min(1), backLabel: z.string().min(1), finderLabel: z.string().min(1),
unavailableMessage: z.string().min(1), settingsMessage: z.string().min(1),
pollIntervalMs: z.number().int().min(100).max(2 ** 31 - 1) }),
operation('requestPermission', { permission: z.enum(['accessibility', 'screenRecording']) }),
operation('openPermissionSettings', { permission: z.enum(['accessibility', 'screenRecording']) }),
] as const
@@ -1,3 +1,3 @@
/** Named Remote boundary types required by Typert's generated Client declarations. */
export type { CuaApplicationId } from './application-types.ts'
export type { CuaApplicationState, CuaSystemPermission, CuaSystemPermissionState, CuaPermissionGuideText } from './access-types.ts'
export type { CuaApplicationState, CuaSystemPermission, CuaSystemPermissionState } from './access-types.ts'
+8 -27
View File
@@ -11,14 +11,6 @@ import type { CuaAppHandle } from '../src/types.ts'
import { NativeClient, NativeError } from '../src/native-client.ts'
import { permissionFixture } from './permission-fixture.ts'
const guideText = {
instruction: 'Drag {application} to the list above to allow Accessibility',
backLabel: 'Back',
finderLabel: 'Show in Finder',
unavailableMessage: 'Application unavailable',
settingsMessage: 'Open System Settings manually',
}
const native = vi.hoisted(() => ({ instances: [] as Array<{
disposed: boolean
request: ReturnType<typeof vi.fn>
@@ -58,7 +50,7 @@ async function setup(choice = 'Allow for this session', options: { writable?: bo
ctx.on('user-questions/request', answer)
const permissions = permissionFixture(ctx, options.initial, options.writable)
const access = installCuaAccess(ctx, { native: nativeConfig, createChannel: unusedConnector, store: permissions.store,
permissionPollIntervalMs: 1000 })
permissionParameters: (method, permission, locale) => ({ method, permission, locale }) })
const client = new NativeClient(ctx, nativeConfig, unusedConnector)
return { ctx, owner, agent: owner.agent, answer, access, client, ...permissions }
}
@@ -169,12 +161,12 @@ it('checks native permissions without converting an OS request into a grant', as
const before = (await f.ctx.cuaAccess.applications())
expect(before).toEqual({ allowedApps: [], blockedApps: [], persistentAvailable: true })
expect(await f.ctx.cuaAccess.systemStatus()).toMatchObject({ accessibility: false, screenRecording: false })
expect(await f.ctx.cuaAccess.requestPermission('accessibility', guideText)).toMatchObject({ accessibility: false })
await f.ctx.cuaAccess.openSettings('screenRecording', guideText.settingsMessage)
expect(await f.ctx.cuaAccess.requestPermission('accessibility', 'en')).toMatchObject({ accessibility: false })
await f.ctx.cuaAccess.openSettings('screenRecording', 'en')
const transport = native.instances[1]!
expect(transport.request).toHaveBeenCalledWith('requestPermission',
{ permission: 'accessibility', ...guideText, pollIntervalMs: 1000 }, expect.any(AbortSignal))
expect(transport.request).toHaveBeenCalledWith('openPermissionSettings', { permission: 'screenRecording' }, expect.any(AbortSignal))
{ method: 'requestPermission', permission: 'accessibility', locale: 'en' }, expect.any(AbortSignal))
expect(transport.request).toHaveBeenCalledWith('openPermissionSettings', { method: 'openPermissionSettings', permission: 'screenRecording', locale: 'en' }, expect.any(AbortSignal))
transport.request.mockRejectedValueOnce(new Error('transport ended'))
await expect(f.ctx.cuaAccess.systemStatus()).rejects.toThrow('transport ended')
transport.disposed = true
@@ -472,22 +464,11 @@ it('does not reactivate old handles when stored permission was revoked and later
expect(f.answer).toHaveBeenCalledOnce()
})
it('localizes settings-open refusal and retains the native cause', async () => {
const f = await setup()
await f.ctx.cuaAccess.systemStatus()
const failure = new NativeError('SETTINGS_UNAVAILABLE', 'Native settings-open failure')
native.instances.at(-1)!.request.mockRejectedValueOnce(failure)
const promise = f.ctx.cuaAccess.openSettings('accessibility', '请手动打开系统设置中的辅助功能')
await expect(promise).rejects.toMatchObject({
code: 'SETTINGS_UNAVAILABLE', message: 'SETTINGS_UNAVAILABLE: 请手动打开系统设置中的辅助功能', cause: failure,
})
})
it.each([new Error('transport ended'), new NativeError('NATIVE_EXIT', 'helper exited')])(
'keeps unrelated settings failures intact: %s', async (failure) => {
it.each([new NativeError('SETTINGS_UNAVAILABLE', '请手动打开系统设置'), new Error('transport ended'), new NativeError('NATIVE_EXIT', 'helper exited')])(
'preserves provider and transport settings failures: %s', async (failure) => {
const f = await setup()
await f.ctx.cuaAccess.systemStatus()
native.instances.at(-1)!.request.mockRejectedValueOnce(failure)
await expect(f.ctx.cuaAccess.openSettings('accessibility', guideText.settingsMessage)).rejects.toBe(failure)
await expect(f.ctx.cuaAccess.openSettings('accessibility', 'en')).rejects.toBe(failure)
},
)
@@ -18,6 +18,7 @@ const provider = {
installCua(ctx, config, {
helperPath: resolve('unused-cua-helper'),
createChannel() { throw new Error('Scope tests must not start a native helper') },
permissionParameters: (_method, permission, locale) => ({ permission, locale }),
description: 'Fixture computer interaction', guidance: 'Fixture CUA guidance',
})
},
@@ -7,14 +7,6 @@ import { expect, it, onTestFinished, vi } from 'vitest'
import type { CuaApplicationId, CuaApplicationState, CuaSystemPermissionState } from '../src/access-types.ts'
import { PermissionController } from '../src/client/controller.ts'
const guideText = {
instruction: '将 {application} 拖入上方列表以允许辅助功能',
backLabel: '返回',
finderLabel: '在 Finder 中显示',
unavailableMessage: '找不到此应用',
settingsMessage: '请手动打开系统设置',
}
const value: CuaSystemPermissionState = {
accessibility: true, screenRecording: false, helper: { pid: 9 }, parent: { pid: 8 } }
const applications: CuaApplicationState = {
@@ -32,9 +24,10 @@ function fixture() {
retryApproval: vi.fn<CuaRemote['retryApproval']>(async () => ({ ok: true, value: undefined })),
enablePersistentPtc: vi.fn<CuaRemote['enablePersistentPtc']>(async () => ({ ok: true, value: undefined })),
}
const controller = new PermissionController(actions, () => guideText)
let locale = 'zh'
const controller = new PermissionController(actions, () => locale)
onTestFinished(() => controller.dispose())
return { actions, controller, face: controller.face() }
return { actions, controller, face: controller.face(), setLocale: (value: string) => { locale = value } }
}
it('refreshes after a request and then waits for an explicit refresh', async () => {
@@ -59,7 +52,7 @@ it('refreshes after a user request without treating the request itself as a gran
const f = fixture()
f.face.request('screenRecording')
await vi.waitFor(() => { expect(f.controller.state.getSnapshot().busy).toBe(false) })
expect(f.actions.requestPermission).toHaveBeenCalledWith('screenRecording', guideText)
expect(f.actions.requestPermission).toHaveBeenCalledWith('screenRecording', 'zh')
expect(f.controller.state.getSnapshot().system.value?.screenRecording).toBe(false)
expect(f.controller.state.getSnapshot().toast?.kind).toBe('requested')
f.face.dismissToast()
@@ -82,7 +75,7 @@ it.each([new Error('write refused'), 'write refused'])('reports settings and rev
const f = fixture()
f.face.openSettings('accessibility')
await vi.waitFor(() => { expect(f.controller.state.getSnapshot().toast?.kind).toBe('opened') })
expect(f.actions.openSettings).toHaveBeenCalledWith('accessibility', guideText.settingsMessage)
expect(f.actions.openSettings).toHaveBeenCalledWith('accessibility', 'zh')
f.face.revoke('a'.repeat(64) as CuaApplicationId)
await vi.waitFor(() => { expect(f.controller.state.getSnapshot().toast?.kind).toBe('revoked') })
f.face.retryApproval('a'.repeat(64) as CuaApplicationId)
@@ -393,3 +386,14 @@ it('preserves settled failures and values while all four observations are paused
}
await f.controller.dispose()
})
it('reads the current Client language for each permission action', async () => {
const f = fixture()
f.face.request('accessibility')
await vi.waitFor(() => { expect(f.controller.state.getSnapshot().busy).toBe(false) })
expect(f.actions.requestPermission).toHaveBeenLastCalledWith('accessibility', 'zh')
f.setLocale('en')
f.face.openSettings('screenRecording')
await vi.waitFor(() => { expect(f.controller.state.getSnapshot().busy).toBe(false) })
expect(f.actions.openSettings).toHaveBeenLastCalledWith('screenRecording', 'en')
})
@@ -94,8 +94,8 @@ it('withdraws the permission Remote and all controls with the optional plugin',
await vi.waitFor(() => { expect(actions.hooks.cuaPermissions.getSnapshot().toast?.kind).toBe('requested') })
actions.openSettings('screenRecording')
await vi.waitFor(() => { expect(actions.hooks.cuaPermissions.getSnapshot().toast?.kind).toBe('opened') })
expect(f.openSettings).toHaveBeenCalledWith('screenRecording',
'Open System Settings > Privacy & Security > Screen Recording manually.')
expect(f.requestPermission).toHaveBeenCalledWith('accessibility', 'en')
expect(f.openSettings).toHaveBeenCalledWith('screenRecording', 'en')
actions.revoke('a'.repeat(64) as import('../src/access-types.ts').CuaApplicationId)
await vi.waitFor(() => { expect(actions.hooks.cuaPermissions.getSnapshot().toast?.kind).toBe('revoked') })
actions.retryApproval('a'.repeat(64) as import('../src/access-types.ts').CuaApplicationId)
@@ -990,15 +990,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
returns: 'Optional native PNG icons keyed by executable path; applications without paths are skipped.',
},
{
signature: '@Remote async requestPermission(permission: CuaSystemPermission, text: CuaPermissionGuideText): Promise<CuaSystemPermissionState>',
description: 'Open the selected System Settings pane; show the native drag guide only for a missing privilege. Guide preparation failures still attempt settings navigation before reporting the failure.',
parameters: [{ name: 'permission', description: 'macOS privilege to request.' }, { name: 'text', description: 'Localized guide copy; application identity comes from the native helper.' }],
returns: 'Current permission observations; opening the guide does not grant permission.',
signature: '@Remote async requestPermission(permission: CuaSystemPermission, locale: string): Promise<CuaSystemPermissionState>',
description: 'Request the selected system privilege through the native provider.',
parameters: [{ name: 'permission', description: 'macOS privilege to request.' }, { name: 'locale', description: 'Current Client language for provider-owned permission UI.' }],
returns: 'Current permission observations; requesting does not establish consent.',
},
{
signature: '@Remote async openSettings(permission: CuaSystemPermission, failureMessage: string): Promise<void>',
signature: '@Remote async openSettings(permission: CuaSystemPermission, locale: string): Promise<void>',
description: 'Open the macOS privacy pane selected by the user.',
parameters: [{ name: 'permission', description: 'Corresponding system privilege.' }, { name: 'failureMessage', description: 'Localized manual navigation instructions if macOS rejects the open request.' }],
parameters: [{ name: 'permission', description: 'Corresponding system privilege.' }, { name: 'locale', description: 'Current Client language for provider-owned recovery instructions.' }],
returns: 'Completion after macOS accepted the open request.',
},
{
@@ -6098,10 +6098,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'CuaApplicationState',
declaration: 'export interface CuaApplicationState {\n allowedApps: readonly CuaApplicationGrant[];\n blockedApps: readonly CuaApplicationGrant[];\n persistentAvailable: boolean;\n}',
},
{
name: 'CuaPermissionGuideText',
declaration: 'export interface CuaPermissionGuideText {\n instruction: string;\n backLabel: string;\n finderLabel: string;\n unavailableMessage: string;\n settingsMessage: string;\n}',
},
{
name: 'CuaPermissionProcess',
declaration: 'export interface CuaPermissionProcess {\n pid: number;\n path?: string | undefined;\n name?: string | undefined;\n}',
@@ -6112,7 +6108,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'CuaSystemPermissionState',
declaration: 'export interface CuaSystemPermissionState {\n accessibility: boolean;\n screenRecording: boolean;\n helper: CuaPermissionProcess;\n parent: CuaPermissionProcess;\n launchApplication?: {\n name: string;\n path: string;\n icon?: string | undefined;\n bundleId?: string | undefined;\n } | undefined;\n}',
declaration: 'export interface CuaSystemPermissionState {\n accessibility: boolean;\n screenRecording: boolean;\n helper: CuaPermissionProcess;\n parent: CuaPermissionProcess;\n launchApplication?: {\n name: string;\n path: string;\n icon?: string | undefined;\n } | undefined;\n}',
},
{
name: 'DailyInput',
+3
View File
@@ -9864,6 +9864,9 @@ importers:
'@deepseek-ai/dsh-cua':
specifier: workspace:*
version: link:../cua
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
zod:
specifier: ^4.4.3
version: 4.4.3
-1
View File
@@ -352,7 +352,6 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
CuaPermissionProcess: 'computer-use.md',
CuaApplicationState: 'computer-use.md',
CuaSystemPermission: 'computer-use.md',
CuaPermissionGuideText: 'computer-use.md',
CuaSystemPermissionState: 'computer-use.md',
RenderedDocumentBytes: 'office-to-pdf.md',
OfficeToPdfRequest: 'office-to-pdf.md',