mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
feat(cua): deliver notarized helpers through on-demand plugin installation
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
name: Package CUA helper · macOS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_ref:
|
||||
description: '同仓库可信源码分支或 dsh-v* tag'
|
||||
type: string
|
||||
default: master
|
||||
required: true
|
||||
preset:
|
||||
description: '机器上的签名配置'
|
||||
type: choice
|
||||
options: [test, production]
|
||||
default: test
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
source_ref:
|
||||
type: string
|
||||
required: true
|
||||
preset:
|
||||
type: string
|
||||
default: production
|
||||
outputs:
|
||||
artifact:
|
||||
description: '包含两个架构的已签名、公证 helper 归档'
|
||||
value: ${{ jobs.package.outputs.artifact }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Desktop uses the same keychain search list and optional system proxy transaction.
|
||||
concurrency:
|
||||
group: macos-desktop-package
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: [self-hosted, macOS, ARM64, dsh-mac-package-trial]
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
artifact: ${{ steps.artifact.outputs.name }}
|
||||
env:
|
||||
DSH_PACKAGING_PRESET: ${{ inputs.preset }}
|
||||
DSH_TELEMETRY_DISABLED: '1'
|
||||
PACKAGE_SOURCE_REF: ${{ inputs.source_ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.source_ref }}
|
||||
persist-credentials: false
|
||||
clean: true
|
||||
- name: Validate source selection
|
||||
run: |
|
||||
node -e 'const cp=require("node:child_process"); const fs=require("node:fs"); const ref=process.env.PACKAGE_SOURCE_REF;
|
||||
if(ref.startsWith("refs/pull/") || /^[a-f0-9]{40}$/i.test(ref)) throw Error("Select a trusted repository branch or release tag");
|
||||
const tag=ref.replace(/^refs\/tags\//, "");
|
||||
if(tag.startsWith("dsh-v") && tag!=="dsh-v"+JSON.parse(fs.readFileSync("package.json","utf8")).version) throw Error("Release tag must match product version");
|
||||
if(!["test","production"].includes(process.env.DSH_PACKAGING_PRESET)) throw Error("Invalid preset");'
|
||||
- uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-cua-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
package-manager-cache: false
|
||||
- name: Install locked dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build and verify notarized applications
|
||||
run: |
|
||||
mkdir -p dist
|
||||
pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper
|
||||
- name: Name retained artifact
|
||||
id: artifact
|
||||
run: echo "name=cua-helper-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: ${{ steps.artifact.outputs.name }}
|
||||
path: dist/cua-helper/*
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
retention-days: 14
|
||||
@@ -16,8 +16,16 @@ env:
|
||||
DSH_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
cua-helper:
|
||||
if: startsWith(github.ref, 'refs/tags/dsh-v')
|
||||
uses: ./.github/workflows/cua-helper-package.yml
|
||||
with:
|
||||
source_ref: ${{ github.ref }}
|
||||
preset: production
|
||||
|
||||
pack:
|
||||
name: Pack npm tarballs
|
||||
needs: cua-helper
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
# Complete history: the release scripts read tags.
|
||||
@@ -60,6 +68,17 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm run build:official
|
||||
|
||||
- uses: actions/download-artifact@v8.0.1
|
||||
with:
|
||||
name: ${{ needs.cua-helper.outputs.artifact }}
|
||||
path: dist/cua-helper
|
||||
|
||||
- name: Stage verified native helper applications
|
||||
run: |
|
||||
mkdir dist/cua-helper/extracted
|
||||
tar -xzf dist/cua-helper/cua-helper.tgz -C dist/cua-helper/extracted
|
||||
pnpm exec tsx scripts/cua-helper/stage.ts --from dist/cua-helper/extracted
|
||||
|
||||
- name: Pack release tarballs
|
||||
run: pnpm run release:pack --family dsh --out dist/npm
|
||||
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"@deepseek-ai/dsh-experimental-terminal-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-session-search": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-personalized-recommendations": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Cold pnpm requests exercise shipped CUA dependency/platform metadata with inert package contents. */
|
||||
/** Cold pnpm requests exercise on-demand CUA dependency/platform metadata with inert package contents. */
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
@@ -37,14 +37,14 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for
|
||||
const cli = await readManifest('apps/cli')
|
||||
const bundle = await readManifest('packages/experimental/cua-bundle')
|
||||
const native = await readManifest('packages/experimental/cua-macos')
|
||||
expect(cli.dependencies).toHaveProperty(bundleName)
|
||||
expect(cli.dependencies).not.toHaveProperty(bundleName)
|
||||
expect(bundle.dependencies).not.toHaveProperty(providerName)
|
||||
expect(bundle.optionalDependencies).toEqual({ [providerName]: 'workspace:*' })
|
||||
expect(native.os).toEqual(['darwin'])
|
||||
const version = '1.0.0'
|
||||
const bundleDependencies = Object.fromEntries(Object.keys(bundle.dependencies as Record<string, string>).map(name => [name, version]))
|
||||
const manifests = [
|
||||
{ name: cli.name, version, dependencies: { [bundleName]: version } },
|
||||
{ name: cli.name, version },
|
||||
{ name: bundleName, version, dependencies: bundleDependencies, optionalDependencies: { [providerName]: version } },
|
||||
...Object.keys(bundleDependencies).map(name => ({ name, version })),
|
||||
{ name: providerName, version, os: native.os, files: native.files },
|
||||
@@ -102,13 +102,13 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for
|
||||
await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') throw new Error('fixture registry has no TCP address')
|
||||
for (const target of ['darwin', 'win32', 'linux', 'linux-unrestricted']) {
|
||||
for (const target of ['darwin-base', 'darwin', 'win32', 'linux', 'linux-unrestricted']) {
|
||||
unrestricted = target === 'linux-unrestricted'
|
||||
const project = join(root, target)
|
||||
await mkdir(project)
|
||||
await writeFile(join(project, 'package.json'), JSON.stringify({ private: true, dependencies: { [String(cli.name)]: version } }))
|
||||
await writeFile(join(project, 'package.json'), JSON.stringify({ private: true, dependencies: { [target === 'darwin-base' ? String(cli.name) : bundleName]: version } }))
|
||||
await writeFile(join(project, 'pnpm-workspace.yaml'), [
|
||||
'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : target}]`,
|
||||
'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : target === 'darwin-base' ? 'darwin' : target}]`,
|
||||
' cpu: [arm64]', 'minimumReleaseAge: 0', '',
|
||||
].join('\n'))
|
||||
requests.length = 0
|
||||
@@ -117,8 +117,8 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for
|
||||
`--config.registry=http://127.0.0.1:${address.port}/`, `--config.store-dir=${join(project, 'store')}`,
|
||||
`--config.cache-dir=${join(project, 'cache')}`, `--config.state-dir=${join(project, 'state')}`,
|
||||
], project)
|
||||
expect(requests).toContain(`/${bundleName}.tgz`)
|
||||
expect(requests).toContain('/@deepseek-ai/dsh-cua.tgz')
|
||||
expect(requests.includes(`/${bundleName}.tgz`)).toBe(target !== 'darwin-base')
|
||||
expect(requests.includes('/@deepseek-ai/dsh-cua.tgz')).toBe(target !== 'darwin-base')
|
||||
expect(requests.includes(`/${providerName}.tgz`), `${target}: ${JSON.stringify(requests)}\n${output}`)
|
||||
.toBe(target === 'darwin' || unrestricted)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
en: 1552866718133b10
|
||||
zh: 1c760c5c2d5cfc0b
|
||||
/authorize-the-computer-use-helper/migration:
|
||||
en: 2a28385e24468734
|
||||
zh: 725c1856a604bc0c
|
||||
en: a79e5995415ff456
|
||||
zh: 6f818a092e53af2c
|
||||
|
||||
@@ -13,7 +13,7 @@ macOS Computer Use runs as `DSH Computer Use.app`, launched through LaunchServic
|
||||
|
||||
## Migration
|
||||
|
||||
1. Restart DSH after upgrading the Computer Use package. In the Computer Use plugin page, grant Accessibility and Screen Recording access to `DSH Computer Use.app` when macOS requests it.
|
||||
1. Computer Use now installs on demand. In **Plugins → Add plugin**, install `@deepseek-ai/dsh-cua-bundle` and enable it; profiles that previously selected the shipped copy need this installation. Restart DSH if requested. In the Computer Use plugin page, grant Accessibility and Screen Recording access to `DSH Computer Use.app` when macOS requests it.
|
||||
2. Remove an obsolete `helperPath` override or `DSH_CUA_HELPER` value to use automatic preparation. For a custom build, point it to the executable at `DSH Computer Use.app/Contents/MacOS/dsh-cua-native` inside the complete application bundle.
|
||||
3. Confirm that the permission page identifies `DSH Computer Use.app` and both permissions are allowed before resuming native application tasks. Target-application consent remains required.
|
||||
4. If a terminal or Desktop launcher was granted these permissions only for Computer Use, revoke those obsolete grants in System Settings. Authorizing the helper does not revoke other applications’ existing permissions; programs the model runs still follow their launcher’s system permissions and the DSH sandbox policy.
|
||||
|
||||
@@ -13,7 +13,7 @@ macOS Computer Use 通过 LaunchServices 以 `DSH Computer Use.app` 运行。终
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 升级 Computer Use 包后重启 DSH。在 Computer Use 插件页按 macOS 提示,给 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。
|
||||
1. Computer Use 改为按需安装。在“插件 → 添加插件”中安装 `@deepseek-ai/dsh-cua-bundle` 并启用;此前选中过内置副本的 profile 也需完成这次安装。按提示重启 DSH。在 Computer Use 插件页按 macOS 提示,给 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。
|
||||
2. 删除过时的 `helperPath` 覆盖配置或 `DSH_CUA_HELPER` 值,使用自动准备。使用自定义构建时,将其指向完整应用包内的 `DSH Computer Use.app/Contents/MacOS/dsh-cua-native` 可执行文件。
|
||||
3. 恢复原生应用任务前,确认权限页显示 `DSH Computer Use.app`,且两项权限均已允许。具体目标应用仍需另行许可。
|
||||
4. 若终端或 Desktop 启动器的这两项权限只用于 Computer Use,可在系统设置中撤销这些旧授权。给 helper 授权不会撤销其他应用已有的权限;模型运行的程序仍遵循启动器的系统权限和 DSH 沙箱策略。
|
||||
|
||||
@@ -522,6 +522,7 @@ export class PluginManager extends TypertRemoteService {
|
||||
catch (error) { platformError = managementError(error) }
|
||||
const error = platformError ?? targetError ?? (enabled ? { code: 'not-bundle' as const } : undefined)
|
||||
bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional,
|
||||
...offered.listedByDefault === undefined ? {} : { listedByDefault: offered.listedByDefault },
|
||||
...platformError?.code === 'unsupported-platform' ? { listedByDefault: false } : {},
|
||||
removable: enabled, rows: [], overrides: [],
|
||||
...error === undefined ? {} : { error } })
|
||||
|
||||
@@ -1776,6 +1776,9 @@ it('lists absent Official entries offline without resolving their packages or co
|
||||
expect(bundles.filter(bundle => bundle.name === entry.packageName)).toEqual([{
|
||||
name: entry.packageName, official: true, availability: 'missing', enabled: false, installed: false, optional: false,
|
||||
removable: false, updatable: false, rows: [], overrides: [], meta: entry.meta,
|
||||
...entry.listedByDefault === undefined ? {} : { listedByDefault: entry.listedByDefault },
|
||||
...entry.platforms !== undefined && !entry.platforms.includes(process.platform)
|
||||
? { listedByDefault: false, error: { code: 'unsupported-platform' } } : {},
|
||||
installTarget: { spec: `${entry.packageName}@${getDshRuntimeVersion()}`, version: getDshRuntimeVersion() },
|
||||
}])
|
||||
expect(resolve.mock.calls.some(([name]) => name === entry.packageName)).toBe(false)
|
||||
|
||||
@@ -1140,7 +1140,8 @@ export class PluginManagerController {
|
||||
return
|
||||
}
|
||||
this.abortInspect()
|
||||
const registry = this.registryMemory.getSnapshot() ?? OFFICIAL_REGISTRY
|
||||
const current = this.getSnapshot().install
|
||||
const registry = current.open ? current.registry : this.registryMemory.getSnapshot() ?? OFFICIAL_REGISTRY
|
||||
const local = target.spec.startsWith('link:')
|
||||
const subject: InstallSubject = {
|
||||
status: 'accepted', kind: local ? 'path' : 'registry', name: pkg.name, version: target.version, bundle: true,
|
||||
@@ -1170,6 +1171,11 @@ export class PluginManagerController {
|
||||
}
|
||||
const listed = state.packages.find(pkg => pkg.name === spec)
|
||||
const catalogSubject = install.subject?.selection === undefined || install.subject.spec !== spec ? undefined : install.subject
|
||||
if (listed?.availability === 'missing' && listed.installTarget !== undefined
|
||||
&& listed.error === undefined && catalogSubject === undefined) {
|
||||
this.openCatalogInstall(listed, listed.installTarget, false)
|
||||
return
|
||||
}
|
||||
if (listed?.availability === 'installation' && listed.optional && listed.listedByDefault === false
|
||||
&& !listed.enabled && listed.error === undefined && listed.readOnlyReason === undefined) {
|
||||
this.abortInspect()
|
||||
|
||||
@@ -1919,6 +1919,22 @@ const CATALOG: BundleInfo = {
|
||||
installTarget: { spec: '@deepseek-ai/dsh-subagent-codex@2.0.0', version: '2.0.0' },
|
||||
}
|
||||
|
||||
it.each(['registry', 'source'] as const)('installs a typed default-hidden Official name using its %s target', async (mode) => {
|
||||
const name = '@deepseek-ai/dsh-cua-bundle'
|
||||
const spec = mode === 'source' ? 'link:/workspace/cua-bundle' : `${name}@2.0.0`
|
||||
const entry = { ...CATALOG, name, listedByDefault: false, installTarget: { spec, version: '2.0.0' } }
|
||||
const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([entry])) })
|
||||
await controller.load()
|
||||
face.openInstall()
|
||||
face.editInstallSpec(name)
|
||||
face.runInstall()
|
||||
await vi.waitFor(() => { expect(plugins.installBundle).toHaveBeenCalledOnce() })
|
||||
expect(plugins.inspect).not.toHaveBeenCalled()
|
||||
expect(plugins.installBundle).toHaveBeenCalledWith(spec, {
|
||||
enabled: false, ...mode === 'registry' ? { saveExact: true } : {}, requestId: state().install.requestId, registry: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('installs an absent Official entry directly at the host target and enables it through the normal installer', async () => {
|
||||
const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG])) })
|
||||
await controller.load()
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: b563e4ec4727d0de
|
||||
zh: d0228da978238e05
|
||||
/deepseek-ai-dsh-cua-bundle/use-this-package:
|
||||
en: dc35841a42753d00
|
||||
zh: 3c8a03a2ecbec26f
|
||||
en: c17e2ac48c68a2fd
|
||||
zh: 90df835c4e9bac75
|
||||
/deepseek-ai-dsh-cua-bundle/understand-the-implementation:
|
||||
en: b1011db52cfc17cd
|
||||
zh: 6fa8798fedc5c0b6
|
||||
@@ -39,8 +39,8 @@
|
||||
en: 417c40acd93df532
|
||||
zh: 69cc5cf7140c75a9
|
||||
/deepseek-ai-dsh-cua-bundle/known-limitations-and-deferred-work:
|
||||
en: 5586cb62bb16d601
|
||||
zh: 8b06372c77d0cd48
|
||||
en: 7d91694e2f6b111e
|
||||
zh: 54bcc0cef9496ed6
|
||||
/deepseek-ai-dsh-cua-bundle/known-limitations-and-deferred-work/dev-note:
|
||||
en: 0a1253c6f7b5e8b9
|
||||
zh: ce96645b3e75874e
|
||||
|
||||
@@ -24,7 +24,7 @@ Enable macOS Computer Use from Plugins for existing and new PTC sessions. Window
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
This workflow targets developers running macOS Web or unpackaged Desktop from source. In **Plugins → Add plugin**, enter `@deepseek-ai/dsh-cua-bundle`, choose **Install**, then **Enable now**. DSH enables its shipped copy without downloading another one. Enabled bundles remain in Plugins, where you can open Computer Use to manage permissions or switch it off. Switching off hides its card again unless the profile separately installed it. The permission page identifies the responsible application reported by macOS when the optional system query is available. Request access and enable that application in System Settings; follow the application named by macOS if it differs from the detected source. For Web, run these commands from the repository root:
|
||||
In macOS Desktop, open **Plugins → Add plugin**, enter `@deepseek-ai/dsh-cua-bundle`, choose **Install**, then **Enable now**. DSH installs the official version matching its runtime into the profile. Published packages contain the signed and notarized helper; users need no compiler. Source Web and development Desktop link the workspace bundle and prepare an ad-hoc helper when enabled. Open Computer Use details, choose PTC if needed, and grant Accessibility and Screen Recording to `DSH Computer Use.app`. Disabled installed bundles remain available for management. For source Web, run these commands from the repository root:
|
||||
|
||||
```sh
|
||||
pnpm run build
|
||||
@@ -85,7 +85,7 @@ CUA activation and removal change the PTC SDK and system guidance; subsequent re
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- Source Web and Desktop share permission controls on macOS. Packaged Desktop helper distribution, signing, and permission attribution are deferred.
|
||||
- First installation requires a reachable registry carrying the matching DSH version. System permissions require the user’s approval in macOS; installation does not grant them.
|
||||
- PTC execution is shared by the profile and is configured independently. A Host without HMR applies saved changes at restart.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -24,7 +24,7 @@ kind: "package-bundle"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
本流程面向从源码运行 macOS Web 或未打包 Desktop 的开发者。在「插件 → 添加插件」中输入 `@deepseek-ai/dsh-cua-bundle`,点击「安装」,再点击「立即启用」。DSH 会启用随应用提供的副本,无需重新下载。已启用的组合包保留在插件页,可打开 Computer Use 管理权限或将其关闭。关闭后卡片再次隐藏,除非该 profile 单独安装了它。权限页通过可选系统查询显示 macOS 报告的责任应用,指引用户申请权限并在系统设置中开启对应开关。若 macOS 显示其他应用名称,以系统提示为准。Web 版在仓库根目录运行以下命令:
|
||||
在 macOS Desktop 的“插件 → 添加插件”中输入 `@deepseek-ai/dsh-cua-bundle`,点击“安装”,再点击“立即启用”。DSH 将与当前运行时版本匹配的官方插件安装到 profile。发布包包含已签名和公证的 helper,用户无需编译工具。源码 Web 和开发 Desktop 链接工作区组合包,启用时准备临时签名的 helper。打开 Computer Use 详情,按需设置 PTC,并为 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。已安装的组合包关闭后仍可管理。源码 Web 在仓库根目录运行以下命令:
|
||||
|
||||
```sh
|
||||
pnpm run build
|
||||
@@ -37,7 +37,7 @@ pnpm dsh web
|
||||
|
||||
关闭会卸载提供方,并从其 Agent 作用域中撤回 CUA 工具。执行器配置、新会话默认值和已保存的授权保留。
|
||||
|
||||
Computer Use 仅支持 macOS DSH Host。原生提供方是仅在 macOS 安装的可选依赖,共享 CUA 和 Client 包仍随安装提供。其他 Host 默认不在插件列表和模型推荐中展示此组合包,并在保存设置前拒绝启用。profile 显式安装的依赖和已保存的选择仍保留管理入口。已选中的组合包会在启动时跳过并报告原因,仍可取消选择;已保存的用户设置保留。其他操作系统上的浏览器可以连接受支持的 Mac Host。
|
||||
Computer Use 仅支持 macOS DSH Host。原生提供方是仅在 macOS 安装的可选依赖,组合包和共享库均按需安装。其他 Host 默认不在插件列表和模型推荐中展示此组合包,并在保存设置前拒绝启用。profile 显式安装的依赖和已保存的选择仍保留管理入口。已选中的组合包会在启动时跳过并报告原因,仍可取消选择;已保存的用户设置保留。其他操作系统上的浏览器可以连接受支持的 Mac Host。
|
||||
|
||||
准备失败时,已保存的选择保留,CUA 操作不可用。刷新或重新打开插件页查看组件诊断,修复后重启 DSH。
|
||||
|
||||
@@ -85,7 +85,7 @@ CUA 生效和卸载会改变 PTC SDK 与系统指引,后续请求使用更新
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- 源码 Web 与 Desktop 共用 macOS 权限控件。桌面安装包的 helper 分发、签名和权限归属留待后续处理。
|
||||
- 首次安装需要能访问提供匹配 DSH 版本的注册表。系统权限需用户在 macOS 中确认,安装插件不会自动授予权限。
|
||||
- PTC 执行方式由 profile 共享,需单独配置。未启用 HMR 的 Host 在重启时应用已保存的变更。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: e8cbc43d38786c40
|
||||
zh: 52b3ba85d98011b9
|
||||
/dsh-cua-native-helper/build-and-service:
|
||||
en: 532ba00d53c5bcb3
|
||||
zh: b5653aaafa2cbc89
|
||||
en: 5c1a2ff8728d4e60
|
||||
zh: db48888732b31c66
|
||||
/dsh-cua-native-helper/request-protocol:
|
||||
en: fc8043c425322cdd
|
||||
zh: cf1edb9a78fda301
|
||||
|
||||
@@ -27,6 +27,10 @@ Run `pnpm --dir packages/experimental/cua-macos run build:native` to publish a s
|
||||
|
||||
Generations in `native/.build/prepared/<arch>` and `native/bin/darwin-<arch>` are retained without automatic garbage collection. Use a fresh `--output-directory` for release staging, or clean the packaging output before building while no client uses it, so old generations do not enter the tarball. Secure signing timestamps can create distinct generations for identical sources.
|
||||
|
||||
The manual **Package CUA helper · macOS** [workflow](../../../../.github/workflows/cua-helper-package.yml) builds both architectures on `dsh-mac-package-trial`. Select a trusted `source_ref` and machine `preset`; credentials use the existing [macOS packaging configuration](../../../../apps/desktop/packaging/README.md#macos-actions). The runner needs an active macOS login session and Rosetta for the x64 startup check. `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` runs the same preparation from the repository root. It signs, notarizes and staples each complete application before selecting its final content digest, then verifies signatures, tickets, Gatekeeper, helper identity and cleanup from the extracted `cua-helper.tgz`. Permission checks do not request access or inspect applications.
|
||||
|
||||
The workflow retains `cua-helper.tgz` and `cua-helper.json`, which binds the product version, source commit, Team ID and both application digests. [Release publish](../../../../.github/workflows/release-publish.yml) consumes this artifact before packing the provider for npm; ordinary PR release rehearsals have no signing credentials. Staging rejects a source/version mismatch, incomplete architectures, stale generations, links, changed bytes and missing executable permissions. npm publication rechecks the exact provider tarball before any registry write. A re-signed release has different bytes; preserve the retained artifact when retrying a partially published version.
|
||||
|
||||
Startup errors retain their code and message through an owner-only, per-attempt failure file and the launcher’s JSON error frame. Launch-lock and readiness waits share one deadline; operating-system launch time consumes that budget. Readiness must name the newly launched PID. A failed new instance is force-terminated and awaited before the launcher reports failure; this cleanup has a separate bound equal to the startup timeout. Existing shared services are not terminated by a failed attaching launcher.
|
||||
|
||||
The application icon is generated from the [Computer Use artwork](../../cua-bundle/icon.svg) with `pnpm --dir packages/experimental/cua-macos run build:icon`. System permission prompts and the DSH permission page use this application icon.
|
||||
|
||||
@@ -27,6 +27,10 @@ kind: "package-reference"
|
||||
|
||||
`native/.build/prepared/<arch>` 和 `native/bin/darwin-<arch>` 中的各代应用不会自动清理。正式打包时使用全新的 `--output-directory`,或确认没有客户端使用后清理打包输出再构建,避免旧应用进入 tarball。安全签名时间戳可能让相同源码生成不同代应用。
|
||||
|
||||
手动工作流 **Package CUA helper · macOS** 的[配置](../../../../.github/workflows/cua-helper-package.yml)在 `dsh-mac-package-trial` 构建两种架构。选择可信的 `source_ref` 和机器 `preset`;凭据沿用[macOS 打包配置](../../../../apps/desktop/packaging/README.zh.md#macos-actions)。runner 需要已登录的 macOS 桌面会话,以及用于 x64 启动检查的 Rosetta。在仓库根目录运行 `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` 可执行相同准备。完整应用签名、公证和贴票后才确定最终内容摘要,再从解包后的 `cua-helper.tgz` 验证签名、票据、Gatekeeper、helper 身份和退出清理。权限查询不会申请授权或观察应用。
|
||||
|
||||
工作流保留 `cua-helper.tgz` 和 `cua-helper.json`,后者记录产品版本、源码 commit、Team ID 和两份应用摘要。[Release publish](../../../../.github/workflows/release-publish.yml)在生成 provider 的 npm 包前接入该产物;普通 PR 发布检查不使用签名凭据。准备步骤拒绝源码或版本不匹配、架构不全、旧应用目录、链接、字节变化和执行权限缺失。npm 发布在写入注册表前重新检查实际 provider tarball。重新签名会改变产物字节,重试部分发布的版本时需保留原产物。
|
||||
|
||||
启动错误通过当前用户专用、每次启动独立的错误文件和 launcher 的 JSON 错误帧保留错误码与消息。等待启动锁和就绪状态共用一个期限,系统启动应用的耗时计入该预算。就绪记录必须指向本次启动的 PID。新实例启动失败后,launcher 强制终止并等待其退出,再报告失败;清理另有与启动超时等长的期限。连接已有服务失败时不会终止该服务。
|
||||
|
||||
应用图标通过 `pnpm --dir packages/experimental/cua-macos run build:icon` 从 [Computer Use 图形](../../cua-bundle/icon.svg)生成。系统授权提示和 DSH 权限页使用这个应用图标。
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"locale/*.json",
|
||||
"lib/index.js",
|
||||
"native/bin",
|
||||
"cua-helper.json",
|
||||
"native/Package.swift",
|
||||
"native/Info.plist",
|
||||
"native/DSHComputerUse.icns",
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
en: 27c22abdfff69a41
|
||||
zh: e60d828cdb75c263
|
||||
/deepseek-ai-dsh-official-bundles/use-this-package/on-demand-bundles:
|
||||
en: 6ba39ba5bfefec79
|
||||
zh: 788a71b5b6f75404
|
||||
en: ba4f412474854ba1
|
||||
zh: 17dae8155c013367
|
||||
/deepseek-ai-dsh-official-bundles/understand-the-implementation:
|
||||
en: 3fb2b58db529d8be
|
||||
zh: 46064459a23d458d
|
||||
|
||||
@@ -41,7 +41,7 @@ Edit this package when the installation's bundle selection or the plugin manager
|
||||
|
||||
### On-demand bundles
|
||||
|
||||
`ON_DEMAND_BUNDLES` names the public Official packages installed only on request. `OFFICIAL_ON_DEMAND_CATALOG` embeds their package-owned localized metadata, icons, and optional `dsh.bundle.platforms` declarations for offline discovery and Host platform checks before installation; `pnpm gen-official-bundle-catalog` explicitly updates generated content, and `verify-official-bundle-catalog` checks resource completeness and freshness; builds consume the committed catalog without importing provider code. The catalog contains no install target: the plugin manager derives it from the running version and installation and uses the ordinary bundle installer. The [product-use check](../../../scripts/verify-product-use.ts) composes shipped and on-demand selections against Web without executing providers.
|
||||
`ON_DEMAND_BUNDLES` names the public Official packages installed only on request. `OFFICIAL_ON_DEMAND_CATALOG` embeds their package-owned localized metadata, icons, and optional `dsh.bundle.platforms` declarations for offline discovery and Host platform checks before installation; `pnpm gen-official-bundle-catalog` explicitly updates generated content, and `verify-official-bundle-catalog` checks resource completeness and freshness; builds consume the committed catalog without importing provider code. The catalog contains no install target: the plugin manager derives it from the running version and installation and uses the ordinary bundle installer. The [product-use check](../../../scripts/verify-product-use.ts) composes shipped and on-demand selections against Web without executing providers. The catalog also preserves `listedByDefault` so a default-hidden bundle stays hidden until explicitly installed or enabled.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ kind: "package-library"
|
||||
|
||||
### 按需组合包
|
||||
|
||||
`ON_DEMAND_BUNDLES` 列出只在请求时安装的官方公开包。`OFFICIAL_ON_DEMAND_CATALOG` 内嵌这些包自己持有的本地化元信息、图标和可选的 `dsh.bundle.platforms` 声明,用于离线发现与安装前的 Host 平台检查;`pnpm gen-official-bundle-catalog` 显式更新生成内容,`verify-official-bundle-catalog` 检查资源完整性与时效;构建使用已提交的目录,不导入提供方代码。目录不包含安装目标:插件管理器根据运行中的版本和安装推导目标,并使用普通的组合包安装器。[产品使用检查](../../../scripts/verify-product-use.ts)针对 Web 组合随附选择和按需选择,不执行提供方。
|
||||
`ON_DEMAND_BUNDLES` 列出只在请求时安装的官方公开包。`OFFICIAL_ON_DEMAND_CATALOG` 内嵌这些包自己持有的本地化元信息、图标和可选的 `dsh.bundle.platforms` 声明,用于离线发现与安装前的 Host 平台检查;`pnpm gen-official-bundle-catalog` 显式更新生成内容,`verify-official-bundle-catalog` 检查资源完整性与时效;构建使用已提交的目录,不导入提供方代码。目录不包含安装目标:插件管理器根据运行中的版本和安装推导目标,并使用普通的组合包安装器。[产品使用检查](../../../scripts/verify-product-use.ts)针对 Web 组合随附选择和按需选择,不执行提供方。 目录也保留 `listedByDefault`,使默认隐藏的组合包在用户显式安装或启用前保持隐藏。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-bas
|
||||
*/
|
||||
export const OPTIONAL_BUNDLES: readonly string[] = [
|
||||
'@deepseek-ai/dsh-personalized-recommendations',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
'@deepseek-ai/dsh-experimental-session-search',
|
||||
'@deepseek-ai/dsh-experimental-ralph-bundle',
|
||||
'@deepseek-ai/dsh-experimental-terminal-bundle',
|
||||
@@ -73,4 +72,5 @@ export const OPTIONAL_BUNDLES: readonly string[] = [
|
||||
export const ON_DEMAND_BUNDLES: readonly string[] = [
|
||||
'@deepseek-ai/dsh-subagent-claude-code',
|
||||
'@deepseek-ai/dsh-subagent-codex',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
]
|
||||
|
||||
@@ -20,4 +20,14 @@ export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] =
|
||||
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K',
|
||||
},
|
||||
},
|
||||
{
|
||||
packageName: '@deepseek-ai/dsh-cua-bundle',
|
||||
platforms: ['darwin'],
|
||||
listedByDefault: false,
|
||||
meta: {
|
||||
title: { 'de': 'Computernutzung', 'en': 'Computer use', 'es': 'Uso de la computadora', 'fr': 'Utilisation de l\'ordinateur', 'ja': 'Computer Use', 'pt': 'Uso do computador', 'ru': '\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0430', 'zh': 'Computer Use' },
|
||||
description: { 'de': 'Unterst\u00FCtzt derzeit macOS; Die Windows-Unterst\u00FCtzung befindet sich in der Entwicklung. L\u00E4dt die Computernutzung f\u00FCr bestehende und neue PTC-Sitzungen. Wechseln Sie in den PTC-Modus und erteilen Sie die Berechtigungen f\u00FCr Barrierefreiheit und Bildschirmaufzeichnung, um ihn zu verwenden.', 'en': 'Currently supports macOS; Windows support is in development. Loads computer use for existing and new PTC sessions. Switch to PTC mode and grant Accessibility and Screen Recording permissions to use it.', 'es': 'Actualmente es compatible con macOS; El soporte para Windows est\u00E1 en desarrollo. Carga el uso de la computadora para sesiones PTC nuevas y existentes. Cambie al modo PTC y otorgue permisos de Accesibilidad y Grabaci\u00F3n de pantalla para usarlo.', 'fr': 'Prend actuellement en charge macOS\u00A0; La prise en charge de Windows est en cours de d\u00E9veloppement. Charge l\'utilisation de l\'ordinateur pour les sessions PTC existantes et nouvelles. Passez en mode PTC et accordez les autorisations d\u2019accessibilit\u00E9 et d\u2019enregistrement d\u2019\u00E9cran pour l\u2019utiliser.', 'ja': '\u73FE\u5728\u306F macOS \u306E\u307F\u5BFE\u5FDC\u3057\u3001Windows \u5BFE\u5FDC\u306F\u958B\u767A\u4E2D\u3067\u3059\u3002\u65E2\u5B58\u304A\u3088\u3073\u65B0\u898F\u306E PTC \u30BB\u30C3\u30B7\u30E7\u30F3\u306B Computer Use \u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3059\u3002PTC \u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u3001\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u3068\u753B\u9762\u53CE\u9332\u306E\u6A29\u9650\u3092\u8A31\u53EF\u3057\u3066\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002', 'pt': 'Atualmente suporta macOS; O suporte Windows est\u00E1 em desenvolvimento. Carrega o uso do computador para sess\u00F5es PTC novas e existentes. Mude para o modo PTC e conceda permiss\u00F5es de acessibilidade e grava\u00E7\u00E3o de tela para us\u00E1-lo.', 'ru': '\u0412 \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0435 \u0432\u0440\u0435\u043C\u044F \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F macOS; \u041F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0430 Windows \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u043A\u0435. \u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0430 \u0434\u043B\u044F \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0445 \u0438 \u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0430\u043D\u0441\u043E\u0432 PTC. \u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u0441\u044C \u0432 \u0440\u0435\u0436\u0438\u043C PTC \u0438 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043D\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0438 \u0437\u0430\u043F\u0438\u0441\u044C \u044D\u043A\u0440\u0430\u043D\u0430 \u0434\u043B\u044F \u0435\u0433\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F.', 'zh': '\u76EE\u524D\u4EC5\u652F\u6301 macOS\uFF0CWindows \u652F\u6301\u6B63\u5728\u5F00\u53D1\u4E2D\u3002\u4E3A\u5DF2\u6709\u53CA\u65B0\u5EFA PTC \u4F1A\u8BDD\u52A0\u8F7D Computer Use\u3002\u8BF7\u5207\u6362\u5230 PTC \u6A21\u5F0F\u6765\u4F7F\u7528\uFF0C\u5E76\u6388\u4E88\u8F85\u52A9\u529F\u80FD\u548C\u5C4F\u5E55\u5F55\u5236\u6743\u9650\u3002' },
|
||||
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8IS0tCiAgICBDb21wdXRlciBVc2UgcGx1Z2luIGljb24uCiAgICBBZGFwdGVkIGZyb20gTHVjaWRlICJtb3VzZS1wb2ludGVyLWNsaWNrIiAoaHR0cHM6Ly9sdWNpZGUuZGV2L2ljb25zL21vdXNlLXBvaW50ZXItY2xpY2spLgogICAgQ2hhbmdlczogc29saWQtZmlsbGVkIHBvaW50ZXIsIHByb2plY3QgcGFsZXR0ZSwgMzZ4MzYgY2FudmFzLgoKICAgIElTQyBMaWNlbnNlCiAgICBDb3B5cmlnaHQgKGMpIDIwMjYgTHVjaWRlIEljb25zIGFuZCBDb250cmlidXRvcnMKICAgIGh0dHBzOi8vZ2l0aHViLmNvbS9sdWNpZGUtaWNvbnMvbHVjaWRlL2Jsb2IvbWFpbi9MSUNFTlNFCgogICAgUGVybWlzc2lvbiB0byB1c2UsIGNvcHksIG1vZGlmeSwgYW5kL29yIGRpc3RyaWJ1dGUgdGhpcyBzb2Z0d2FyZSBmb3IgYW55CiAgICBwdXJwb3NlIHdpdGggb3Igd2l0aG91dCBmZWUgaXMgaGVyZWJ5IGdyYW50ZWQsIHByb3ZpZGVkIHRoYXQgdGhlIGFib3ZlCiAgICBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgogICAgVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIFRIRSBBVVRIT1IgRElTQ0xBSU1TIEFMTCBXQVJSQU5USUVTCiAgICBXSVRIIFJFR0FSRCBUTyBUSElTIFNPRlRXQVJFIElOQ0xVRElORyBBTEwgSU1QTElFRCBXQVJSQU5USUVTIE9GCiAgICBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1IgQkUgTElBQkxFIEZPUgogICAgQU5ZIFNQRUNJQUwsIERJUkVDVCwgSU5ESVJFQ1QsIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyBPUiBBTlkgREFNQUdFUwogICAgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOCiAgICBBQ1RJT04gT0YgQ09OVFJBQ1QsIE5FR0xJR0VOQ0UgT1IgT1RIRVIgVE9SVElPVVMgQUNUSU9OLCBBUklTSU5HIE9VVCBPRgogICAgT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBVU0UgT1IgUEVSRk9STUFOQ0UgT0YgVEhJUyBTT0ZUV0FSRS4KICAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0LjAzNiA0LjAzNikgc2NhbGUoMS4yMjAxKSI+CiAgICA8cGF0aCBkPSJNOS4wMzcgOS42OWEuNDk4LjQ5OCAwIDAgMSAuNjUzLS42NTNsMTEgNC41YS41LjUgMCAwIDEtLjA3NC45NDlsLTQuMzQ5IDEuMDQxYTEgMSAwIDAgMC0uNzQuNzM5bC0xLjA0IDQuMzVhLjUuNSAwIDAgMS0uOTUuMDc0eiIgZmlsbD0idXJsKCNnKSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIC8+CiAgICA8cGF0aCBkPSJNMTQgNC4xIDEyIDYiIHN0cm9rZT0iIzUxQ0ZEQiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIC8+CiAgICA8cGF0aCBkPSJtNS4xIDgtMi45LS44IiBzdHJva2U9IiM1MUNGREIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiAvPgogICAgPHBhdGggZD0ibTYgMTItMS45IDIiIHN0cm9rZT0iIzUxQ0ZEQiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIC8+CiAgICA8cGF0aCBkPSJNNy4yIDIuMiA4IDUuMSIgc3Ryb2tlPSIjNTFDRkRCIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgLz4KICA8L2c+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSI5IiB5MT0iOSIgeDI9IjIxIiB5Mj0iMjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzg2QjhGNSIgLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTI2OUUxIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+Cjwvc3ZnPgo=',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface OfficialBundleCatalogEntry {
|
||||
readonly packageName: string
|
||||
/** Package-owned Host platform declaration, available before installation. */
|
||||
readonly platforms?: DshBundleManifest['platforms']
|
||||
/** Package-owned discovery visibility before installation. */
|
||||
readonly listedByDefault?: boolean
|
||||
/** Package-owned localized text and a self-contained icon, embedded without provider code. */
|
||||
readonly meta: Required<Pick<PluginLocalizedMeta, 'title' | 'description' | 'icon'>>
|
||||
}
|
||||
|
||||
Generated
+3
-6
@@ -325,9 +325,6 @@ importers:
|
||||
'@deepseek-ai/dsh-cordis-client-runner':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/extensions/cordis-client-runner
|
||||
'@deepseek-ai/dsh-cua-bundle':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/experimental/cua-bundle
|
||||
'@deepseek-ai/dsh-experimental-agent-team-profile':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/experimental/agent-team-profile
|
||||
@@ -747,12 +744,12 @@ importers:
|
||||
'@deepseek-ai/dsh-home-paths':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/util/home-paths
|
||||
'@deepseek-ai/dsh-plugin-manager':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/boot/plugin-manager
|
||||
'@deepseek-ai/dsh-official-bundles':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/util/official-bundles
|
||||
'@deepseek-ai/dsh-plugin-manager':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/boot/plugin-manager
|
||||
'@deepseek-ai/dsh-util-file-drag':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/util/file-drag
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/** Build company-signed, notarized CUA applications using the existing macOS packaging credentials. */
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { execa } from 'execa'
|
||||
import { create, extract } from 'tar'
|
||||
import { loadDesktopPackageEnvironment } from '../../apps/desktop/scripts/desktop-package-environment.mjs'
|
||||
import { resolveMacOSNotarizationEnvironment, resolveMacOSSigningEnvironment } from '../../apps/desktop/scripts/desktop-release-environment.mjs'
|
||||
import { withMacOSSigningKeychain } from '../../apps/desktop/scripts/macos-signing-keychain.mjs'
|
||||
import { withMacOSNotarizationProxy } from '../../apps/desktop/scripts/macos-notarization-proxy.ts'
|
||||
import { resolveMacOSPackageSettings } from '../../apps/desktop/scripts/macos-package-settings.mjs'
|
||||
import { notarizeMacOS } from '../../apps/desktop/scripts/notarize-macos.mjs'
|
||||
import { verifyMacOSRuntimeCode } from '../../apps/desktop/scripts/verify-macos-signature.mjs'
|
||||
import { CUA_APPLICATION, CUA_ARCHITECTURES, CUA_MANIFEST, readCuaApplication, sealCuaApplication, verifyCuaHelperPayload, type CuaHelperManifest } from './payload.ts'
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({ options: { out: { type: 'string' } }, allowPositionals: false })
|
||||
if (process.platform !== 'darwin' || !values.out) throw new Error('Usage on macOS: package.ts --out <new-output-directory>')
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const output = resolve(values.out)
|
||||
const environment = loadDesktopPackageEnvironment('darwin')
|
||||
const signing = resolveMacOSSigningEnvironment(environment)
|
||||
const notary = resolveMacOSNotarizationEnvironment(environment)
|
||||
const settings = resolveMacOSPackageSettings(environment)
|
||||
const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string }
|
||||
const commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim()
|
||||
mkdirSync(output, { recursive: false })
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'dsh-cua-package-'))
|
||||
const controller = new AbortController()
|
||||
const stop = (): void => { controller.abort() }
|
||||
process.once('SIGINT', stop)
|
||||
process.once('SIGTERM', stop)
|
||||
const run = async (command: string, args: string[], env = environment): Promise<void> => {
|
||||
await execa(command, args, { cwd: root, env, extendEnv: false, stdio: 'inherit', cancelSignal: controller.signal, killDescendants: true })
|
||||
}
|
||||
const verify = async (application: string, architecture: 'arm64' | 'x64'): Promise<void> => {
|
||||
verifyMacOSRuntimeCode(application, signing)
|
||||
await run('/usr/bin/lipo', ['-verify_arch', architecture === 'x64' ? 'x86_64' : 'arm64', join(application, 'Contents/MacOS/dsh-cua-native')])
|
||||
await run('/usr/bin/xcrun', ['stapler', 'validate', application])
|
||||
await run('/usr/sbin/spctl', ['--assess', '--type', 'execute', '--verbose=4', application])
|
||||
}
|
||||
try {
|
||||
const payload = join(scratch, 'payload')
|
||||
const applications: CuaHelperManifest['applications'] = []
|
||||
await withMacOSSigningKeychain(environment, async (signingEnvironment) => {
|
||||
for (const architecture of CUA_ARCHITECTURES) {
|
||||
controller.signal.throwIfAborted()
|
||||
const directory = join(payload, 'native/bin', `darwin-${architecture}`)
|
||||
await run('/usr/bin/xcrun', ['python3', join(root, 'packages/experimental/cua-macos/native/build_app.py'),
|
||||
'--arch', architecture === 'x64' ? 'x86_64' : 'arm64', '--scratch-path', join(scratch, `swift-${architecture}`),
|
||||
'--output-directory', directory, '--identity', `Developer ID Application: ${signing.signingIdentity}`], signingEnvironment)
|
||||
const { application } = readCuaApplication(directory)
|
||||
await withMacOSNotarizationProxy(settings.notarizationProxy, () => notarizeMacOS({ appPath: application, ...notary }))
|
||||
await verify(application, architecture)
|
||||
applications.push({ architecture, bundle: sealCuaApplication(directory, application) })
|
||||
}
|
||||
})
|
||||
const manifest: CuaHelperManifest = { schemaVersion: 1, version, commit, teamId: signing.teamId, applications }
|
||||
writeFileSync(join(payload, CUA_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
verifyCuaHelperPayload(payload, { version, commit })
|
||||
const archive = join(output, 'cua-helper.tgz')
|
||||
await create({ cwd: payload, file: archive, gzip: true, portable: true }, [CUA_MANIFEST, 'native'])
|
||||
const extracted = join(scratch, 'extracted')
|
||||
mkdirSync(extracted)
|
||||
await extract({ cwd: extracted, file: archive, strict: true })
|
||||
verifyCuaHelperPayload(extracted, { version, commit })
|
||||
for (const { architecture, bundle } of applications) {
|
||||
const application = join(extracted, 'native/bin', `darwin-${architecture}`, bundle, CUA_APPLICATION)
|
||||
await verify(application, architecture)
|
||||
await run('/usr/bin/xcrun', ['python3', join(root, 'scripts/cua-helper/smoke.py'), join(application, 'Contents/MacOS/dsh-cua-native')])
|
||||
}
|
||||
writeFileSync(join(output, CUA_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(`CUA helper artifact: ${archive}`)
|
||||
} catch (error) {
|
||||
rmSync(join(output, 'cua-helper.tgz'), { force: true })
|
||||
throw error
|
||||
} finally {
|
||||
process.off('SIGINT', stop)
|
||||
process.off('SIGTERM', stop)
|
||||
rmSync(scratch, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -0,0 +1,86 @@
|
||||
/** Final application bytes and source identity survive packaging; invalid inputs never reach publication. */
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { create } from 'tar'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { CUA_APPLICATION, CUA_ARCHITECTURES, CUA_MANIFEST, cuaApplicationDigest, readCuaApplication, sealCuaApplication, stageCuaHelperPayload, verifyCuaHelperPayload } from './payload.ts'
|
||||
import { verifyCuaHelperTarball } from './verify-tarball.ts'
|
||||
|
||||
const identity = { version: '1.0.0', commit: 'a'.repeat(40) }
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cua-payload-test-'))
|
||||
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
|
||||
const payload = join(root, 'payload')
|
||||
const applications = CUA_ARCHITECTURES.map((architecture) => {
|
||||
const directory = join(payload, 'native/bin', `darwin-${architecture}`)
|
||||
const application = join(directory, 'unsigned', CUA_APPLICATION)
|
||||
for (const [path, bytes] of Object.entries({
|
||||
'Contents/Info.plist': 'fixture identity', 'Contents/MacOS/dsh-cua-native': `executable ${architecture}`,
|
||||
'Contents/Resources/DSHComputerUse.icns': 'icon', 'Contents/_CodeSignature/CodeResources': 'signature',
|
||||
})) {
|
||||
mkdirSync(join(application, path, '..'), { recursive: true })
|
||||
writeFileSync(join(application, path), bytes)
|
||||
}
|
||||
chmodSync(join(application, 'Contents/MacOS/dsh-cua-native'), 0o755)
|
||||
return { architecture, bundle: sealCuaApplication(directory, application) }
|
||||
})
|
||||
const manifest = { schemaVersion: 1, ...identity, teamId: 'ABCDEFGHIJ', applications }
|
||||
writeFileSync(join(payload, CUA_MANIFEST), JSON.stringify(manifest))
|
||||
return { root, payload, manifest, directory: join(payload, 'native/bin/darwin-arm64') }
|
||||
}
|
||||
|
||||
it.skipIf(process.platform === 'win32')('matches the native build receipt hash and includes post-signing ticket bytes', () => {
|
||||
const f = fixture()
|
||||
const before = readCuaApplication(f.directory)
|
||||
const script = resolve('packages/experimental/cua-macos/native/build_app.py')
|
||||
const digest = execFileSync('python3', ['-c', 'import runpy,sys; from pathlib import Path; print(runpy.run_path(sys.argv[1])["bundle_digest"](Path(sys.argv[2])))', script, before.application], { encoding: 'utf8' }).trim()
|
||||
expect(cuaApplicationDigest(before.application)).toBe(digest)
|
||||
writeFileSync(join(before.application, 'Contents/ticket'), 'notarization ticket')
|
||||
expect(() => readCuaApplication(f.directory)).toThrow('digest')
|
||||
const after = sealCuaApplication(f.directory, before.application)
|
||||
expect(after).not.toBe(before.bundle)
|
||||
expect(readFileSync(join(readCuaApplication(f.directory).application, 'Contents/ticket'), 'utf8')).toBe('notarization ticket')
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('verifies staged and repacked provider bytes before publication', async () => {
|
||||
const f = fixture()
|
||||
expect(verifyCuaHelperPayload(f.payload, identity)).toEqual(f.manifest)
|
||||
const packageRoot = join(f.root, 'package')
|
||||
stageCuaHelperPayload(f.payload, packageRoot, identity)
|
||||
expect(() => stageCuaHelperPayload(f.payload, packageRoot, identity)).toThrow('empty native/bin')
|
||||
const tarball = join(f.root, 'provider.tgz')
|
||||
await create({ file: tarball, cwd: f.root, gzip: true, portable: true }, ['package'])
|
||||
await verifyCuaHelperTarball(tarball, identity)
|
||||
const application = readCuaApplication(join(packageRoot, 'native/bin/darwin-arm64')).application
|
||||
writeFileSync(join(application, 'Contents/MacOS/dsh-cua-native'), 'changed after signing')
|
||||
await create({ file: tarball, cwd: f.root, gzip: true, portable: true }, ['package'])
|
||||
await expect(verifyCuaHelperTarball(tarball, identity)).rejects.toThrow('digest')
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32').each(['version', 'commit', 'architecture', 'missing', 'extra', 'permissions', 'symlink'] as const)('rejects %s damage before staging', (damage) => {
|
||||
const f = fixture()
|
||||
const before = readCuaApplication(f.directory)
|
||||
if (damage === 'version') f.manifest.version = '2.0.0'
|
||||
if (damage === 'commit') f.manifest.commit = 'b'.repeat(40)
|
||||
if (damage === 'architecture') f.manifest.applications[1] = f.manifest.applications[0]!
|
||||
if (damage === 'missing') rmSync(join(before.application, 'Contents/Info.plist'))
|
||||
if (damage === 'extra') cpSync(before.application, join(f.directory, 'old-generation', CUA_APPLICATION), { recursive: true })
|
||||
if (damage === 'permissions') chmodSync(join(before.application, 'Contents/MacOS/dsh-cua-native'), 0o644)
|
||||
if (damage === 'symlink') symlinkSync(join(f.root, 'outside'), join(before.application, 'outside'))
|
||||
writeFileSync(join(f.payload, CUA_MANIFEST), JSON.stringify(f.manifest))
|
||||
expect(() => stageCuaHelperPayload(f.payload, join(f.root, 'consumer'), identity)).toThrow()
|
||||
})
|
||||
|
||||
it('keeps company signing outside pull-request release rehearsals', () => {
|
||||
const workflow = readFileSync(resolve('.github/workflows/cua-helper-package.yml'), 'utf8')
|
||||
expect(workflow).toContain('workflow_dispatch:')
|
||||
expect(workflow).toContain('workflow_call:')
|
||||
expect(workflow).not.toContain('pull_request:')
|
||||
expect(workflow).toContain('dsh-mac-package-trial')
|
||||
const release = readFileSync(resolve('.github/workflows/release-publish.yml'), 'utf8')
|
||||
expect(release).toContain('needs: cua-helper')
|
||||
expect(release).toContain('scripts/cua-helper/stage.ts')
|
||||
expect(readFileSync(resolve('.github/workflows/release.yml'), 'utf8')).not.toContain('cua-helper-package.yml')
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
/** Integrity and source binding for notarized Computer Use applications consumed by npm packaging. */
|
||||
import { createHash } from 'node:crypto'
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { z } from 'zod'
|
||||
|
||||
/** Architectures carried by each published macOS provider package. */
|
||||
export const CUA_ARCHITECTURES = ['arm64', 'x64'] as const
|
||||
/** Native application name shared with the provider receipt. */
|
||||
export const CUA_APPLICATION = 'DSH Computer Use.app'
|
||||
/** Completion record written only after both applications pass distribution checks. */
|
||||
export const CUA_MANIFEST = 'cua-helper.json'
|
||||
const Digest = z.string().regex(/^[a-f0-9]{64}$/u)
|
||||
const Manifest = z.object({
|
||||
schemaVersion: z.literal(1), version: z.string().min(1), commit: z.string().regex(/^[a-f0-9]{40}$/u),
|
||||
teamId: z.string().regex(/^[A-Z0-9]{10}$/u),
|
||||
applications: z.array(z.object({ architecture: z.enum(CUA_ARCHITECTURES), bundle: Digest }).strict()).length(2),
|
||||
}).strict()
|
||||
/** Verified build identity and application generations. */
|
||||
export type CuaHelperManifest = z.infer<typeof Manifest>
|
||||
|
||||
/**
|
||||
* Hash application-relative file names, bytes and executable bits, matching build_app.py.
|
||||
* @param application - Real application directory; links and special files are rejected.
|
||||
* @returns SHA-256 of the complete application, including its notarization ticket.
|
||||
*/
|
||||
export function cuaApplicationDigest(application: string): string {
|
||||
const root = lstatSync(application)
|
||||
if (!root.isDirectory() || root.isSymbolicLink()) throw new Error('CUA application must be a real directory')
|
||||
const files: string[] = []
|
||||
const visit = (directory: string): void => {
|
||||
for (const name of readdirSync(join(application, directory))) {
|
||||
const path = directory ? `${directory}/${name}` : name
|
||||
const stat = lstatSync(join(application, path))
|
||||
if (stat.isDirectory()) visit(path)
|
||||
else if (stat.isFile()) files.push(path)
|
||||
else throw new Error(`CUA application contains a link or special file: ${path}`)
|
||||
}
|
||||
}
|
||||
visit('')
|
||||
const digest = createHash('sha256')
|
||||
for (const path of files.sort()) {
|
||||
const file = join(application, path)
|
||||
digest.update(`${path}\0`).update(createHash('sha256').update(readFileSync(file)).digest())
|
||||
.update(Buffer.from([(lstatSync(file).mode & 0o111) === 0 ? 0 : 1]))
|
||||
}
|
||||
return digest.digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the complete signed application and validate its immutable receipt.
|
||||
* @param directory - One architecture's native/bin directory.
|
||||
* @returns Selected application path and its content digest.
|
||||
*/
|
||||
export function readCuaApplication(directory: string): { application: string; bundle: string } {
|
||||
const { bundle } = z.object({ bundle: Digest }).strict().parse(JSON.parse(readFileSync(join(directory, 'current.json'), 'utf8')))
|
||||
const application = join(directory, bundle, CUA_APPLICATION)
|
||||
for (const path of ['Contents/Info.plist', 'Contents/MacOS/dsh-cua-native', 'Contents/Resources/DSHComputerUse.icns', 'Contents/_CodeSignature/CodeResources']) {
|
||||
if (!lstatSync(join(application, path)).isFile()) throw new Error(`CUA application omits ${path}`)
|
||||
}
|
||||
if ((lstatSync(join(application, 'Contents/MacOS/dsh-cua-native')).mode & 0o111) === 0) throw new Error('CUA helper is not executable')
|
||||
if (cuaApplicationDigest(application) !== bundle) throw new Error('CUA application digest does not match current.json')
|
||||
if (readdirSync(directory).sort().join(',') !== [bundle, 'current.json'].sort().join(',')) throw new Error('CUA release contains unselected application generations')
|
||||
return { application, bundle }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reselect a build after notarization has added its ticket, before distribution.
|
||||
* @param directory - Fresh, build-owned architecture directory.
|
||||
* @param application - Application just notarized and verified by the producer.
|
||||
* @returns Final content digest; old build receipts are replaced.
|
||||
*/
|
||||
export function sealCuaApplication(directory: string, application: string): string {
|
||||
const bundle = cuaApplicationDigest(application)
|
||||
const destination = join(directory, bundle, CUA_APPLICATION)
|
||||
if (destination !== application) {
|
||||
mkdirSync(join(directory, bundle))
|
||||
renameSync(application, destination)
|
||||
for (const name of readdirSync(directory)) {
|
||||
if (name !== bundle && name !== 'current.json') rmSync(join(directory, name), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
writeFileSync(join(directory, 'current.json'), `${JSON.stringify({ bundle })}\n`)
|
||||
readCuaApplication(directory)
|
||||
return bundle
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the producer record and every final application before copying or packing.
|
||||
* @param root - Extracted helper artifact containing native/bin and cua-helper.json.
|
||||
* @param expected - Source commit and product version of the consuming checkout.
|
||||
* @returns Validated producer metadata.
|
||||
*/
|
||||
export function verifyCuaHelperPayload(root: string, expected: { version: string; commit: string }): CuaHelperManifest {
|
||||
const manifest = Manifest.parse(JSON.parse(readFileSync(join(root, CUA_MANIFEST), 'utf8')))
|
||||
if (manifest.version !== expected.version || manifest.commit !== expected.commit) throw new Error('CUA artifact does not match the release source and version')
|
||||
if (new Set(manifest.applications.map(entry => entry.architecture)).size !== 2) throw new Error('CUA artifact must contain arm64 and x64')
|
||||
const bin = join(root, 'native/bin')
|
||||
if (readdirSync(bin).sort().join(',') !== 'darwin-arm64,darwin-x64') throw new Error('CUA artifact has unexpected architecture directories')
|
||||
for (const entry of manifest.applications) {
|
||||
const { bundle } = readCuaApplication(join(bin, `darwin-${entry.architecture}`))
|
||||
if (bundle !== entry.bundle) throw new Error('CUA application differs from the producer manifest')
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage verified native applications for the provider's ordinary pnpm pack command.
|
||||
* @param root - Extracted helper artifact.
|
||||
* @param packageRoot - Provider package to receive native/bin.
|
||||
* @param expected - Source identity required by the release.
|
||||
*/
|
||||
export function stageCuaHelperPayload(root: string, packageRoot: string, expected: { version: string; commit: string }): void {
|
||||
verifyCuaHelperPayload(root, expected)
|
||||
const destination = join(packageRoot, 'native/bin')
|
||||
if (existsSync(destination)) throw new Error('CUA staging requires an empty native/bin; use a clean release checkout')
|
||||
cpSync(join(root, 'native/bin'), destination, { recursive: true })
|
||||
cpSync(join(root, CUA_MANIFEST), join(packageRoot, CUA_MANIFEST))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Query a distributed helper without requesting system permissions or inspecting applications."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
executable = Path(sys.argv[1]).resolve(strict=True)
|
||||
key = hashlib.sha256(executable.read_bytes()).hexdigest()
|
||||
pid = None
|
||||
with tempfile.TemporaryDirectory(prefix="cua-smoke-", dir="/tmp") as directory:
|
||||
os.chmod(directory, 0o700)
|
||||
try:
|
||||
started = subprocess.run([str(executable), "--ensure-shared", "--runtime-key", key,
|
||||
"--runtime-directory", directory, "--startup-timeout-ms", "15000"],
|
||||
check=True, capture_output=True, text=True, timeout=25)
|
||||
ready = json.loads(started.stdout)
|
||||
assert ready["runtimeKey"] == key and ready["type"] == "ready", ready
|
||||
pid = ready["pid"]
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(20)
|
||||
connection.connect(f"{directory}/{key}.sock")
|
||||
stream = connection.makefile("rwb", buffering=0)
|
||||
hello = {"type": "hello", "protocol": 1, "runtimeKey": key, "config": {
|
||||
"maxRequestBytes": 1048576, "maxResponseBytes": 33554432, "axTimeoutMs": 3000,
|
||||
"textEventIntervalMs": 0.25, "maxClickHoldMs": 5000, "screenshotTimeoutMs": 5000,
|
||||
"timeoutMs": 30000, "graceMs": 5000,
|
||||
}}
|
||||
stream.write((json.dumps(hello) + "\n").encode())
|
||||
admitted = json.loads(stream.readline())
|
||||
assert admitted["type"] == "ready" and admitted["pid"] == pid, admitted
|
||||
for method in ["getCapabilities", "permissionStatus"]:
|
||||
stream.write((json.dumps({"id": method, "method": method, "params": {}}) + "\n").encode())
|
||||
response = json.loads(stream.readline())
|
||||
assert response["id"] == method and "result" in response, response
|
||||
result = response["result"]
|
||||
if method == "getCapabilities":
|
||||
assert result["platform"] == "macos", result
|
||||
else:
|
||||
assert result["launchApplication"]["name"] == "DSH Computer Use.app", result.get("launchApplication", {}).get("name")
|
||||
assert isinstance(result["accessibility"], bool) and isinstance(result["screenRecording"], bool)
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
assert stream.read() == b"", "helper did not acknowledge connection cleanup"
|
||||
stream.close()
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
pid = None
|
||||
break
|
||||
time.sleep(0.025)
|
||||
assert pid is None, "helper remained alive after its final connection closed"
|
||||
print("CUA distribution smoke: capabilities, helper identity and cleanup passed")
|
||||
finally:
|
||||
if pid is not None:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
subprocess.run(["/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
|
||||
"-u", str(executable.parents[2])], check=True, capture_output=True, timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Stage the signed helper artifact selected by the credential-free npm pack job. */
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { stageCuaHelperPayload } from './payload.ts'
|
||||
|
||||
const { values } = parseArgs({ options: { from: { type: 'string' } }, allowPositionals: false })
|
||||
if (!values.from) throw new Error('Usage: stage.ts --from <extracted-helper-artifact>')
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const { version } = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { version: string }
|
||||
const commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim()
|
||||
stageCuaHelperPayload(resolve(values.from), resolve(root, 'packages/experimental/cua-macos'), { version, commit })
|
||||
console.log(`CUA helper staged for ${version} (${commit})`)
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Reject incomplete or modified CUA payloads before any npm publication. */
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { extract } from 'tar'
|
||||
import { verifyCuaHelperPayload } from './payload.ts'
|
||||
|
||||
/**
|
||||
* Verify both helper applications in the exact provider tarball being published.
|
||||
* @param tarball - pnpm-produced provider package archive.
|
||||
* @param expected - Product version and source commit of the publishing checkout.
|
||||
* @returns Resolves only when the archive has complete, digest-verified application generations.
|
||||
*/
|
||||
export async function verifyCuaHelperTarball(tarball: string, expected: { version: string; commit: string }): Promise<void> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cua-publish-verify-'))
|
||||
try {
|
||||
await extract({ file: tarball, cwd: root, strict: true, filter: (path, entry) => {
|
||||
if (!path.startsWith('package/') || path.split('/').includes('..')
|
||||
|| !['File', 'Directory'].includes(entry.type)) throw new Error(`Invalid CUA package entry: ${path}`)
|
||||
return true
|
||||
} })
|
||||
verifyCuaHelperPayload(join(root, 'package'), expected)
|
||||
} finally { rmSync(root, { recursive: true, force: true }) }
|
||||
}
|
||||
@@ -81,6 +81,17 @@ describe('Official bundle catalog generation', () => {
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
|
||||
})
|
||||
|
||||
it('preserves default-hidden discovery before installation', () => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', listedByDefault: false } } })
|
||||
expect(generateOfficialBundleCatalog(root)[0]?.listedByDefault).toBe(false)
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(readFileSync(join(root, output), 'utf8')).toContain('listedByDefault: false')
|
||||
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', listedByDefault: 'hidden' } } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow('listedByDefault must be a boolean')
|
||||
})
|
||||
|
||||
it.each([[], ['macos'], 'darwin'].map(platforms => ({ platforms })))('rejects invalid package platform metadata $platforms', ({ platforms }) => {
|
||||
const root = fixture()
|
||||
updateManifest(root, ON_DEMAND_BUNDLES[0]!, { dsh: { bundle: { patch: './cordis.patch.yml', platforms } } })
|
||||
|
||||
@@ -22,7 +22,7 @@ const OUTPUT = 'packages/util/official-bundles/src/catalog.generated.ts'
|
||||
*/
|
||||
export function generateOfficialBundleCatalog(root: string, names: readonly string[] = ON_DEMAND_BUNDLES): OfficialBundleCatalogEntry[] {
|
||||
if (names.length === 0 || new Set(names).size !== names.length) throw new Error('Official on-demand catalog must contain distinct package names')
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; platforms?: DshBundleManifest['platforms']; runtime: string[] }>()
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; platforms?: DshBundleManifest['platforms']; listedByDefault?: boolean; runtime: string[] }>()
|
||||
for (const path of globSync(['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json'], { cwd: root }).map(path => path.replaceAll('\\', '/')).sort()) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as {
|
||||
name: string
|
||||
@@ -30,12 +30,15 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms'] } }
|
||||
dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms']; listedByDefault?: boolean } }
|
||||
}
|
||||
if (manifests.has(manifest.name)) throw new Error(`duplicate workspace package ${manifest.name}`)
|
||||
const listedByDefault = manifest.dsh?.bundle?.listedByDefault
|
||||
if (listedByDefault !== undefined && typeof listedByDefault !== 'boolean') throw new Error(`${manifest.name}: dsh.bundle.listedByDefault must be a boolean`)
|
||||
const runtime = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
manifests.set(manifest.name, { path, private: manifest.private === true, bundle: manifest.dsh?.bundle?.patch !== undefined,
|
||||
...manifest.dsh?.bundle?.platforms === undefined ? {} : { platforms: manifest.dsh.bundle.platforms },
|
||||
...listedByDefault === undefined ? {} : { listedByDefault },
|
||||
runtime: runtime.flatMap(section => Object.keys(manifest[section] ?? {})) })
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
@@ -77,7 +80,9 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
}
|
||||
}
|
||||
}
|
||||
return { packageName, ...entry.platforms === undefined ? {} : { platforms: entry.platforms },
|
||||
return { packageName,
|
||||
...entry.listedByDefault === undefined ? {} : { listedByDefault: entry.listedByDefault },
|
||||
...entry.platforms === undefined ? {} : { platforms: entry.platforms },
|
||||
meta: { title: meta.title, description: meta.description, icon: meta.icon } }
|
||||
})
|
||||
}
|
||||
@@ -95,6 +100,7 @@ export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCata
|
||||
+ entries.map(entry => ' {\n'
|
||||
+ ` packageName: ${quote(entry.packageName)},\n`
|
||||
+ (entry.platforms === undefined ? '' : ` platforms: [${entry.platforms.map(quote).join(', ')}],\n`)
|
||||
+ (entry.listedByDefault === undefined ? '' : ` listedByDefault: ${entry.listedByDefault},\n`)
|
||||
+ ' meta: {\n'
|
||||
+ ` title: ${localized(entry.meta.title)},\n`
|
||||
+ ` description: ${localized(entry.meta.description)},\n`
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
* the same artifact safe.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { verifyCuaHelperTarball } from '../cua-helper/verify-tarball.ts'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
@@ -184,6 +186,11 @@ export async function publishRelease(familyId: string, directory: string, distTa
|
||||
const tarball = join(directory, filename)
|
||||
return { tarball, ...packedIdentity(tarball) }
|
||||
})
|
||||
for (const member of order) {
|
||||
if (member.name !== '@deepseek-ai/dsh-experimental-cua-macos') continue
|
||||
const commit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
||||
await verifyCuaHelperTarball(member.tarball, { version: member.version, commit })
|
||||
}
|
||||
if (distTag !== undefined) {
|
||||
for (const member of order) verifyDistTag(member, distTag)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user