diff --git a/.github/workflows/macos-intel-package.yml b/.github/workflows/macos-intel-package.yml index cc835b8933..4f60c1b2a7 100644 --- a/.github/workflows/macos-intel-package.yml +++ b/.github/workflows/macos-intel-package.yml @@ -127,6 +127,23 @@ jobs: args=() if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi pnpm run package:desktop:mac:x64 --check --build-version "$PACKAGE_VERSION" "${args[@]}" + - name: Build and stage notarized Computer Use helpers + # A source whose dsh ships the Computer Use bundle packs the workspace provider into the Desktop runtime, + # which therefore needs the same notarized helper npm publication stages. + run: | + if ! node -e "process.exit(require('./apps/cli/package.json').dependencies?.['@deepseek-ai/dsh-cua-bundle'] ? 0 : 1)"; then + echo 'This source does not ship Computer Use in Desktop; no helper is staged.' + exit 0 + fi + if [[ ! -f scripts/cua-helper/package.ts ]]; then + echo '::warning::This source ships Computer Use but predates helper packaging; Computer Use cannot prepare its helper in this package.' + exit 0 + fi + mkdir -p dist + pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper + 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: Build signed installers env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 073f1e9541..eff4ebe10d 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -124,6 +124,23 @@ jobs: args=() if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi pnpm run package:desktop:mac:arm64 --check --build-version "$PACKAGE_VERSION" "${args[@]}" + - name: Build and stage notarized Computer Use helpers + # A source whose dsh ships the Computer Use bundle packs the workspace provider into the Desktop runtime, + # which therefore needs the same notarized helper npm publication stages. + run: | + if ! node -e "process.exit(require('./apps/cli/package.json').dependencies?.['@deepseek-ai/dsh-cua-bundle'] ? 0 : 1)"; then + echo 'This source does not ship Computer Use in Desktop; no helper is staged.' + exit 0 + fi + if [[ ! -f scripts/cua-helper/package.ts ]]; then + echo '::warning::This source ships Computer Use but predates helper packaging; Computer Use cannot prepare its helper in this package.' + exit 0 + fi + mkdir -p dist + pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper + 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: Build signed installers env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} diff --git a/apps/cli/package.json b/apps/cli/package.json index c194a246b9..3df78de1fb 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -106,6 +106,7 @@ "@deepseek-ai/dsh-experimental-liveshare": "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:*", "@deepseek-ai/dsh-experimental-forge-bundle": "workspace:*" diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 3dfceaccd1..899796c03a 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -15,8 +15,8 @@ en: 127ea90553cb6af7 zh: 7b1c5aec8558c684 /deepseek-harness-desktop/application-overview: - en: 1667e3d6ffddea4a - zh: 4661a42b9ef4f673 + en: d4afa46403997884 + zh: 9b9fe126afda70f0 /deepseek-harness-desktop/terminal-command: en: 5f2d25a1d8472647 zh: 2acb5c5d7d1917e2 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index fe24b24568..bf36a7b9fe 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -35,7 +35,7 @@ Electron caches the last confirmed UI language in `userData/desktop/locale-cache Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited. -The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s `3080` or a reserved port; a `webserver.config.port` patch can override it. +The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s `3080` or a reserved port; a `webserver.config.port` patch can override it. Native Host requests send the current Host's authentication cookie explicitly, and no Electron session stores it; each Host start removes `dsh-auth-*` cookies that earlier releases left in the default session, because cookies ignore ports and would otherwise accompany every request to `127.0.0.1`. Desktop binds `127.0.0.1`, the address Electron dials for the ready URL and the WebSocket credential filter. WebSocket streams use `ws:` or `wss:` according to the Host listener; credentials require matching authority and scheme. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 933e329ce1..6241072116 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -35,7 +35,7 @@ Electron 在 `userData/desktop/locale-cache` 缓存最近确认的界面语言 桌面埋点遵循[产品采集策略](../../packages/client/product-analytics/README.zh.md)及其动态应用配置,不包含 Web 使用情况。安装更新会等待该操作的本地埋点接收请求结束,再锁定 API 准入并停止 Host。接收请求的时限为一秒,失败不会阻止安装,也不等待收集端完成发送。 -桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认监听系统分配的端口,因此不会与 Web 的 `3080` 或系统保留端口冲突;可通过 `webserver.config.port` patch 覆盖。 +桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认监听系统分配的端口,因此不会与 Web 的 `3080` 或系统保留端口冲突;可通过 `webserver.config.port` patch 覆盖。原生 Host 请求会显式发送当前 Host 的认证 Cookie,任何 Electron session 都不保存该 Cookie;每次启动 Host 时都会移除早期版本遗留在默认 session 中的 `dsh-auth-*` Cookie,因为 Cookie 不区分端口,否则这些 Cookie 会随每个发往 `127.0.0.1` 的请求一并发送。 Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过滤器拨号的地址。WebSocket 流按 Host 监听器使用 `ws:` 或 `wss:`;附加凭据要求 authority 与 scheme 均匹配。 diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index c39bb71bc3..49412773f7 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -27,17 +27,17 @@ en: 253e17998951931c zh: b582b377391d2ec2 /desktop-packaging/macos-self-hosted-packaging: - en: f4cc5209bdaeefba - zh: 1db959d1ed345fcc + en: b7e03e2e9807a62c + zh: e5b0c43910e085d8 /desktop-packaging/unsigned-windows-test-installer: en: f429bc0c280e2dfa zh: 4adbcc9e906bdf80 /desktop-packaging/local-macos-dmg-without-release-signing: - en: 2feaa0412247d81f - zh: 461dde8e13fd4734 + en: 1146bbc11be43960 + zh: 7c13543a0513c059 /desktop-packaging/macos-release-signing-and-diagnostics: - en: 29fa6bd8919f3ec0 - zh: 9e31e037fa3d9378 + en: a0b4aa1080ecf6fb + zh: 37d4b8934f89bee8 /desktop-packaging/directory-builds-and-preparation: en: 5860c306375ddd63 zh: a76e3cdbbe25c79e @@ -45,11 +45,11 @@ en: b42f103358d0dfd7 zh: a534164ad9eb00da /desktop-packaging/runtime-file-selection-and-installation-checks: - en: 83e1075417039410 - zh: 3088a7ae0a2cc3c5 + en: 44044a5c14b75533 + zh: 39d91a369c4ec2fa /desktop-packaging/desktop-runtime-preparation: - en: ab73b67d899e611d - zh: d77b0525c6b81e91 + en: 84be38ab3136fff7 + zh: bb4ed12ee0e1dfa0 /desktop-packaging/installation-and-release-verification: en: eb35eb973b796643 zh: c1c52311632b7296 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 28aa9ff9c7..502a95e2be 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -125,7 +125,7 @@ Installer uploads store the already-compressed release files without ZIP recompr ## macOS self-hosted packaging -The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. +The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. All three platform forms put source, environment, update channel, version mode, manual version, test release ID and compression level first, with matching defaults. macOS notarization and Windows signing and cache measurement options follow these common fields. @@ -172,19 +172,21 @@ pnpm run package:desktop:mac:x64:unsigned Add `--check` to validate local configuration and tools without building, or `--dir` to stop at the unpacked application. The DMG is `apps/desktop/.desktop-build/targets//unsigned-artifacts/deepseek-harness--mac--unsigned.dmg`. After all checks pass, the command prints `DMG:` followed by its absolute path. Open it and copy the application to a local test directory. -These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application. +These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application. These builds stage no Computer Use helper, so Computer Use reports the missing helper when it prepares. ## macOS release signing and diagnostics -The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. Ephemeral CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job; self-hosted machine presets retain private files outside the checkout. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- `. +The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file outside the notarized Computer Use helper bundles; for those bundles it verifies the deep signature, release identity, stapled ticket, and Gatekeeper acceptance without re-signing their files. A signed local build of a source that ships Computer Use therefore needs the notarized helper staged first with `scripts/cua-helper/package.ts` and `stage.ts`; an ad-hoc `build:native` output under `native/bin` fails this verification; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. Ephemeral CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job; self-hosted machine presets retain private files outside the checkout. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- `. macOS signing visits real files without following Framework symlink aliases. PAK resources retain all shipped languages and are sealed by the enclosing Framework or application signature instead of receiving individual signatures. The [release policy](../../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md) owns the dependency patch and verification requirements. macOS runtime preparation caches verified single-architecture Mach-O signatures in `.desktop-build/targets//signature-cache` unless `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` selects external storage. An explicit path must be absolute and already exist, contain no linked ancestors, and be owned by the current account with mode `0700` and read/write/search access; invalid or empty settings fail preflight without fallback. Omit the field to retain target-local storage. Keys include input bytes and permissions, the signing probe’s actual leaf certificate, identifier, entitlement bytes, macOS version, and signing tools and policy. Git commits do not determine reuse: uncommitted byte changes invalidate the affected file. Every hit verifies cached bytes, the strict signature, certificate, identifier, entitlements, secure timestamp and hardened runtime before replacing an unchanged input. Universal binaries are signed each time. Runtime integrity and smoke checks, App signing, and Apple notarization still run. The cache requires trusted local build storage. Cache corruption or unsafe links fail the build; stop packaging before deleting the affected cache directory and retrying. Successful signing stages prune complete entries to a one-GiB payload budget; interrupted temporary entries require manual removal. Concurrent eviction can fail a reader safely. Logs report hit, miss and uncached counts. -Mac package commands print `DESKTOP_PACKAGING_RECORD` with a unique directory under `apps/desktop/.desktop-build/packaging-runs/`. After reading local configuration, each run retains `run.json` (published version, product version, Git commit, dirty-worktree flag, target and Node version), `events.jsonl` (timestamped phases, durations, parallel output attribution and proxy restoration), redacted child `stdout.log` / `stderr.log`, and `result.json` (overall outcome, stage results and artifact directory). Events also record pack concurrency and whether each proxy is configured. Logs survive failures and later builds; no automatic deletion runs. Missing `result.json` means completion is unconfirmed. Known credential values are redacted; environments and notarytool authentication arguments are not recorded. Runtime preparation records package staging, installation, materialization, signing, manifests, smoke checks, descriptor verification and cleanup separately. Nested phase durations overlap and must not be added together. +Mac package commands print `DESKTOP_PACKAGING_RECORD` with a unique directory under `apps/desktop/.desktop-build/packaging-runs/`. After reading local configuration, each run retains `run.json` (published version, product version, Git commit, dirty-worktree flag, target and Node version), `events.jsonl` (timestamped phases, durations, parallel output attribution and proxy restoration), redacted child `stdout.log` / `stderr.log`, and `result.json` (overall outcome, stage results and artifact directory). Events also record pack concurrency and whether each proxy is configured. Logs survive failures and later builds; no automatic deletion runs. Missing `result.json` means completion is unconfirmed. Known credential values are redacted, and environments are not recorded. Apple command failures retain their exit code, signal, stdout and stderr in redacted form; the adapter also writes these details to stderr when no journal is available. Runtime preparation records package staging, installation, materialization, signing, manifests, smoke checks, descriptor verification and cleanup separately. Nested phase durations overlap and must not be added together. + +CUA helper packaging prints `CUA_PACKAGING_RECORD` and creates its own record in the same directory before desktop installer startup. Its `run.json` identifies the helper target, product version and source commit; `events.jsonl` retains command and notarization phases, submission IDs and Apple results. `result.json` records success or failure. These records survive temporary payload cleanup, and the Desktop Actions diagnostic upload includes them even when helper preparation fails. App and DMG notarization each record a submission ID and separate `notarytool:upload:*` and `notarytool:wait:*` durations. Upload uses `submit --no-wait` and includes authentication, local validation, transfer and server acceptance; it is not pure transfer time. Waiting starts after that command returns and includes polling and remaining Apple processing; Apple can start processing before upload submission returns. Apple status and diagnostic logs are retained, including rejection; signature checks, acceptance checks and stapling remain owned by `@electron/notarize`. Directory-only package commands use this same measured App notarization path. Individual subprocess output remains live in the terminal, while phase failures and nested errors are retained in the journal. Both platforms record parent-process packaging failures and print redacted diagnostics, including proxy recovery instructions, to the terminal. The configuration-only `check:package` command creates no run log. @@ -246,7 +248,7 @@ Desktop packs workspace packages locally and installs external dependencies thro The [Electron shell file list](../scripts/electron-builder-config.mjs) excludes recognized JavaScript/CSS/TypeScript source maps and TypeScript build caches from the separate `resources/app.asar/node_modules` dependency collection. These exclusions preserve runtime code, native modules, WASM, data files (including ordinary `.map` assets), licenses, and notices; [shell archive tests](../tests/shell-file-selection.spec.ts) check retained bytes and exclusions. -The [Office conversion provider](../../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in). +The [Office conversion provider](../../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in). The Computer Use helper applications under `@deepseek-ai/dsh-experimental-cua-macos/native/bin` are also unpacked whole under `resources/app.asar.unpacked/dsh/node_modules/`, and the provider opens that copy. The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](../tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](../tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](../scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also obtains CLI paths from the loaded skill and runs capabilities and DOCX conversion with an empty PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization. @@ -280,7 +282,7 @@ Desktop [patch policy](../scripts/runtime-patch-policy.ts) explicitly classifies Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with `--frozen-lockfile`; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration. -Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. +Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files, and verify the notarized Computer Use helper bundles without re-signing them, before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. Windows runtime sealing reads at most eight files concurrently, before and after native signing. A read failure drains outstanding reads without replacing the descriptor; successful sealing still requires the subsequent complete integrity and smoke checks. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 2e87933826..7f43a0c2e5 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -125,7 +125,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s ## macOS 自托管打包 -[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 +[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 三个平台的表单均依次展示源码、环境、升级通道、版本模式、手动版本号、测试 release ID 和压缩等级,默认值一致。macOS 公证选项和 Windows 签名、缓存测量选项位于公共字段之后。 @@ -172,19 +172,21 @@ pnpm run package:desktop:mac:x64:unsigned 添加 `--check` 仅验证本地配置和工具而不构建,或添加 `--dir` 只生成解包应用。DMG 路径为 `apps/desktop/.desktop-build/targets//unsigned-artifacts/deepseek-harness--mac--unsigned.dmg`。所有检查通过后,命令打印 `DMG:` 及其绝对路径。打开后将应用复制到本地测试目录。 -这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。 +这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。这类构建不放入 Computer Use helper,Computer Use 准备时会报告 helper 缺失。 ## macOS 发布签名与诊断 -macOS 签名配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 签名打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 签名固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。签名的仅目录命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。临时 CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者;自托管机器 preset 在 checkout 外保留私有文件。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- ` 重复应用检查时,也必须提供两个 macOS 身份变量。 +macOS 签名配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 签名打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到已公证 Computer Use helper 应用包之外的每个内嵌 Mach-O 文件;对这些应用包,则校验深度签名、发布身份、钉票和 Gatekeeper 评估结果,不重签其中的文件。因此,打包会随附 Computer Use 的源码时,本地签名构建需要先用 `scripts/cua-helper/package.ts` 和 `stage.ts` 放入已公证的 helper;`native/bin` 下由 `build:native` 生成的临时签名产物无法通过此校验;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 签名固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。签名的仅目录命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。临时 CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者;自托管机器 preset 在 checkout 外保留私有文件。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- ` 重复应用检查时,也必须提供两个 macOS 身份变量。 macOS 签名遍历真实文件,不跟随 Framework 的软链接别名。PAK 资源保留全部随附语言,由外层 Framework 或应用签名记录完整性,不逐个签名。[发布策略](../../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md)负责依赖补丁和验证要求。 macOS 运行时准备将已验证的单架构 Mach-O 签名缓存在 `.desktop-build/targets//signature-cache`,除非通过 `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` 选择外置存储。显式路径必须是已存在的绝对目录,祖先路径不能包含链接,目录须归当前账户所有、权限为 `0700` 且可读写及遍历;非法或空配置在预检时失败,不回退。省略该字段保留目标本地缓存。缓存键包含输入字节与权限、签名探针实际使用的叶证书、签名标识、entitlement 字节、macOS 版本,以及签名工具与策略。复用不取决于 Git 提交:未提交的字节变更会使对应文件失效。每次命中都验证缓存字节、严格签名、证书、标识、entitlements、安全时间戳和 hardened runtime,再替换未被并发修改的输入。通用二进制每次重新签名。运行时完整性与 smoke 检查、App 签名和 Apple 公证仍会执行。缓存要求受信任的本地构建存储。缓存损坏或不安全链接会令构建失败;停止打包后删除对应缓存目录再重试。成功的签名阶段会将完整缓存条目的内容总量裁剪至一 GiB;中断留下的临时条目需手动清理。并发淘汰可能使读取方安全失败。日志记录命中、未命中及未缓存数量。 -Mac 打包命令通过 `DESKTOP_PACKAGING_RECORD` 输出 `apps/desktop/.desktop-build/packaging-runs/` 下的唯一目录。读取本地配置后,每次运行保留 `run.json`(发布版本、产品版本、Git 提交、工作区是否有改动、目标及 Node 版本)、`events.jsonl`(带时间戳的阶段、耗时、并行输出归属及代理恢复状态)、脱敏后的子进程 `stdout.log` / `stderr.log`,以及 `result.json`(整体结果、阶段结果和产物目录)。事件还记录打包并发数及各代理是否配置。失败和后续打包不会清除日志;没有自动删除流程。缺少 `result.json` 表示完成状态未经确认。已知凭据值会被遮盖;不记录环境变量全集或 notarytool 认证参数。运行时准备分别记录包暂存、安装、依赖树复制、签名、清单生成、smoke 检查、描述文件校验和清理。嵌套阶段的耗时存在重叠,不能直接相加。 +Mac 打包命令通过 `DESKTOP_PACKAGING_RECORD` 输出 `apps/desktop/.desktop-build/packaging-runs/` 下的唯一目录。读取本地配置后,每次运行保留 `run.json`(发布版本、产品版本、Git 提交、工作区是否有改动、目标及 Node 版本)、`events.jsonl`(带时间戳的阶段、耗时、并行输出归属及代理恢复状态)、脱敏后的子进程 `stdout.log` / `stderr.log`,以及 `result.json`(整体结果、阶段结果和产物目录)。事件还记录打包并发数及各代理是否配置。失败和后续打包不会清除日志;没有自动删除流程。缺少 `result.json` 表示完成状态未经确认。已知凭据值会被遮盖,不记录环境变量全集。Apple 命令失败时保留脱敏后的退出码、信号、stdout 和 stderr;没有日志目录时,适配器也会将这些详情写入 stderr。运行时准备分别记录包暂存、安装、依赖树复制、签名、清单生成、smoke 检查、描述文件校验和清理。嵌套阶段的耗时存在重叠,不能直接相加。 + +CUA helper 打包通过 `CUA_PACKAGING_RECORD` 输出同一目录下的独立记录路径,在桌面安装包开始构建前创建。其 `run.json` 记录 helper 目标、产品版本和源码提交,`events.jsonl` 保留命令与公证阶段、已返回的提交 ID 和 Apple 处理结果,`result.json` 记录成功或失败。这些记录不会随临时 payload 清理而删除;即使 helper 准备失败,Desktop Actions 的诊断上传步骤也会收集它们。 App 和 DMG 公证分别记录 submission ID,以及独立的 `notarytool:upload:*` 和 `notarytool:wait:*` 耗时。上传使用 `submit --no-wait`,包含认证、本地校验、传输和服务端受理,并非纯传输时间。等待从该命令返回后开始,包含轮询及剩余的 Apple 处理时间;Apple 可能在上传提交命令返回前已开始处理。日志保留 Apple 状态与诊断信息,包括拒绝结果;签名检查、接受状态校验和 stapling 仍由 `@electron/notarize` 负责。仅生成目录的打包命令也使用同一条可计时的 App 公证路径。各子进程输出仍实时显示在终端,阶段失败及嵌套错误保留在事件日志中。两个平台均记录父进程打包失败,并在终端显示脱敏诊断,包括代理恢复操作指引。仅检查配置的 `check:package` 命令不创建运行日志。 @@ -246,7 +248,7 @@ Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安 [Electron 外壳文件清单](../scripts/electron-builder-config.mjs)从单独收集的 `resources/app.asar/node_modules` 依赖中排除已识别的 JavaScript/CSS/TypeScript source map 和 TypeScript 构建缓存。这些排除规则保留运行时代码、原生模块、WASM、数据文件(包括普通 `.map` 资源)、许可证和 notices;[外壳归档测试](../tests/shell-file-selection.spec.ts)检查保留文件的字节和排除项。 -[Office 转换提供方](../../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。 +[Office 转换提供方](../../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。`@deepseek-ai/dsh-experimental-cua-macos/native/bin` 下的 Computer Use helper 应用也整体解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下,提供方打开该副本。 打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](../tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](../tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](../scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。 @@ -280,7 +282,7 @@ Desktop [补丁策略](../scripts/runtime-patch-policy.ts) 将每份工作区补 Desktop 依赖 overrides 在补丁策略模块中单独声明,不从补丁版本推导。pi-ai 约束使运行时版本保持在共享补丁已验证的版本。Desktop 仍会在每次构建时重新解析运行时锁文件,再通过 `--frozen-lockfile` 安装;不同构建之间的依赖解析尚不保证可复现。CLI 打包安装测试及其他交付流程保留各自的配置。 -每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 +每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,并在不重签的前提下校验已公证的 Computer Use helper 应用包,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 Windows 运行时在原生签名前后封存文件清单,每次最多并发读取八个文件。读取失败时会等待未完成的读取结束,不替换描述符;封存成功后仍须完成后续的完整性与 smoke 检查。 diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index cf0c19e895..0e12399d89 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -76,7 +76,10 @@ export function createElectronBuilderConfig( let dshDestination let windowsCode = [] const unpack = ['**/*.{node,dylib,dll,so,exe}', '**/*.so.*', '**/spawn-helper', '**/@vscode/ripgrep-*/bin/rg', - `**/node_modules/@deepseek-ai/libreoffice-kit-${resolvedPlatform}-${resolvedArch}/**/*`] + `**/node_modules/@deepseek-ai/libreoffice-kit-${resolvedPlatform}-${resolvedArch}/**/*`, + // LaunchServices opens the Computer Use helper as an application, so every file of its bundles stays on disk. + // Both architectures ship: they form one notarized payload whose manifest staging verifies as a whole. + '**/node_modules/@deepseek-ai/dsh-experimental-cua-macos/native/bin/**/*'] const windowsSigner = packagesWindows && !unsigned ? createWindowsTokenSigner({ certificateFile: env.DSH_DESKTOP_WINDOWS_CER_FILE, diff --git a/apps/desktop/scripts/logged-notarytool.mjs b/apps/desktop/scripts/logged-notarytool.mjs index ae2a9bf055..a9ed964b3c 100755 --- a/apps/desktop/scripts/logged-notarytool.mjs +++ b/apps/desktop/scripts/logged-notarytool.mjs @@ -5,7 +5,7 @@ import { basename, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { promisify } from 'node:util' import { recordPackagingEvent, packagingOutputRedactor } from './packaging-run.mjs' -import { packagingStep } from './packaging-step.mjs' +import { packagingErrorDetails, packagingStep } from './packaging-step.mjs' const exec = promisify(execFile) const AUTH_FLAGS = new Set(['--apple-id', '--password', '--team-id', '--key', '--key-id', '--issuer', '--keychain', '--keychain-profile']) @@ -17,7 +17,8 @@ async function execute(args) { } catch (error) { // Apple can report rejection with a nonzero exit; electron-notarize needs its JSON ID to retrieve diagnostics. if (args[0] === 'wait' && typeof error.code === 'number' && typeof error.stdout === 'string' && error.stdout.trim()) return error.stdout - throw error + throw new Error(`notarytool ${args[0]} failed (code: ${String(error.code ?? 'unknown')}, signal: ${String(error.signal ?? 'none')})\n` + + `stdout: ${error.stdout ?? ''}\nstderr: ${error.stderr ?? ''}`, { cause: error }) } } @@ -74,10 +75,13 @@ export async function runLoggedNotarytool(args, directory, invoke = execute) { } if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { - try { process.stdout.write(await runLoggedNotarytool(process.argv.slice(2), process.env.DSH_DESKTOP_PACKAGING_RUN_DIR)) } - catch { - // execFile errors include credential-bearing argv; detailed failures are redacted in the journal. - process.stderr.write('desktop notarization: Apple command failed; see packaging events.jsonl\n') + const args = process.argv.slice(2) + const directory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + try { process.stdout.write(await runLoggedNotarytool(args, directory)) } + catch (error) { + const credentials = args.filter((_, index) => AUTH_FLAGS.has(args[index - 1])) + process.stderr.write(`desktop notarization: Apple command failed${directory ? `; see ${directory}/events.jsonl` : ''}\n` + + `${packagingErrorDetails(error, credentials)}\n`) process.exitCode = 1 } } diff --git a/apps/desktop/scripts/macos-runtime.ts b/apps/desktop/scripts/macos-runtime.ts index 1d4c27c0d5..16b8fe2a27 100644 --- a/apps/desktop/scripts/macos-runtime.ts +++ b/apps/desktop/scripts/macos-runtime.ts @@ -8,9 +8,15 @@ import { inventoryDesktopRuntime } from '../src/runtime-tree.ts' import type { MacOSSigningEnvironment } from './desktop-release-environment.mjs' import { cachedMacOSSignature, pruneMacOSSignatureCache } from './macos-signature-cache.ts' import { macOSCachePolicy } from './macos-cache-policy.ts' -import { signMacOSRuntimeCode, verifyMacOSRuntimeCode } from './verify-macos-signature.mjs' +import { signMacOSRuntimeCode, verifyMacOSNotarizedApplication, verifyMacOSRuntimeCode } from './verify-macos-signature.mjs' const MACH_O_MAGICS = new Set(['cafebabe', 'cafebabf', 'cefaedfe', 'cffaedfe', 'feedface', 'feedfacf', 'bebafeca', 'bfbafeca']) +/** + * The notarized Computer Use helper bundle containing a runtime path, as staged by `scripts/cua-helper/stage.ts`: + * `native/bin/darwin-//.app`. Other bundles, such as LibreOffice's unsigned application, + * are not matched and keep per-file runtime signing. + */ +const CUA_HELPER_BUNDLE = /^(node_modules\/@deepseek-ai\/dsh-experimental-cua-macos\/native\/bin\/darwin-[^/]+\/[^/]+\/[^/]+\.app)\//u function magic(path: string): string { const descriptor = openSync(path, 'r') @@ -33,21 +39,32 @@ async function primaryRuntimeFiles(root: string): Promise { } /** - * Sign and verify every materialized Mach-O file, awaiting all signers on failure. + * Sign and verify every materialized Mach-O file, awaiting all signers on failure. Files inside the notarized Computer + * Use helper bundles are not re-signed, because signing them breaks the bundle seal and changes the identity macOS + * grants permissions to; each such bundle's deep signature, release identity, stapled ticket, and Gatekeeper + * acceptance are verified before any file is signed. * @param root - Self-contained production runtime. * @param appId - Release application identifier. * @param expected - Required signing identity. * @param arch - Target runtime architecture, independent of the signing host. * @param cacheDirectory - Optional content-addressed cache; requires the keychain-owned signing probe. * @param kind - Primary payloads admit validated Python aliases; Desktop application runtimes reject every link. - * @returns Number of signed native files. + * @returns Number of signed native files, excluding files inside verified Computer Use helper bundles. + * @throws When a helper bundle fails verification, or when any signer fails. */ export async function signMacOSRuntime( root: string, appId: string, expected: MacOSSigningEnvironment, arch: 'arm64' | 'x64', cacheDirectory?: string, kind: 'desktop' | 'primary' = 'desktop', ): Promise { const inventory = kind === 'primary' ? await primaryRuntimeFiles(root) : inventoryDesktopRuntime(root).map(file => file.path) - const files = inventory.filter(path => MACH_O_MAGICS.has(magic(join(root, path)))) + const bundles = new Set() + const files: string[] = [] + for (const path of inventory) { + const bundle = CUA_HELPER_BUNDLE.exec(path)?.[1] + if (bundle !== undefined) bundles.add(bundle) + else if (MACH_O_MAGICS.has(magic(join(root, path)))) files.push(path) + } + for (const bundle of [...bundles].sort()) verifyMacOSNotarizedApplication(join(root, bundle), expected) const policy = cacheDirectory === undefined ? undefined : macOSCachePolicy(process.env.DSH_DESKTOP_MACOS_SIGNING_PROBE ?? '') let hits = 0 let misses = 0 diff --git a/apps/desktop/src/account-backend.ts b/apps/desktop/src/account-backend.ts index 862b8f81ed..19ae3fe796 100644 --- a/apps/desktop/src/account-backend.ts +++ b/apps/desktop/src/account-backend.ts @@ -80,10 +80,10 @@ export interface DesktopAccountBackend { * Connect native account operations to the standard authenticated Web backend. * @param origin - Host Web origin. * @param invoke - validated unary RPC caller. - * @param cookies - Electron session cookie reader. + * @param cookie - Host authentication Cookie header for the account WebSocket. * @returns account operations; watch callers own their subscriptions. */ -export function desktopAccountBackend(origin: string, invoke: AccountInvoke, cookies: () => Promise): DesktopAccountBackend { +export function desktopAccountBackend(origin: string, invoke: AccountInvoke, cookie: string): DesktopAccountBackend { const call = async (method: string, args: Record = {}): Promise => accountView(await invoke({ namespace: 'account', method, args })) return { @@ -98,34 +98,31 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo const streamId = randomUUID() const expiryStreamId = randomUUID() const analyticsPolicyStreamId = randomUUID() - void cookies().then((cookie) => { - if (closed) return - const url = new URL(REMOTE_STREAM_MUX_PATH, origin) - url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' - socket = new WebSocket(url, { headers: { cookie, origin }, maxPayload: 65_536 }) - socket.on('open', () => { - if (onAnalyticsEnabledChanged !== undefined) socket?.send(JSON.stringify({ type: 'open', streamId: analyticsPolicyStreamId, endpoint: 'productAnalytics/watchPolicy', payload: { args: {} } })) - socket?.send(JSON.stringify({ type: 'open', streamId: expiryStreamId, endpoint: 'account/watchExpiry', payload: { args: {} } })) - socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } })) - }) - socket.on('message', (data) => { - try { - const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data) - const frame = parseRemoteStreamServerMessage(bytes.toString('utf8')) - if (frame.streamId === analyticsPolicyStreamId) { - if (frame.type === 'item' && typeof frame.value === 'boolean') onAnalyticsEnabledChanged?.(frame.value) - else onAnalyticsEnabledChanged?.(false) - return - } - if (frame.streamId === expiryStreamId && frame.type === 'item' && frame.value === 'session-expired') { expired(); return } - if (frame.streamId !== streamId) throw new Error('desktop account: unexpected stream') - if (frame.type === 'item') listener(accountView(frame.value)) - else socket?.close() - } catch { socket?.close() } - }) - socket.on('error', () => { socket?.close() }) - socket.on('close', () => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } }) - }).catch(() => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } }) + const url = new URL(REMOTE_STREAM_MUX_PATH, origin) + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' + socket = new WebSocket(url, { headers: { cookie, origin }, maxPayload: 65_536 }) + socket.on('open', () => { + if (onAnalyticsEnabledChanged !== undefined) socket?.send(JSON.stringify({ type: 'open', streamId: analyticsPolicyStreamId, endpoint: 'productAnalytics/watchPolicy', payload: { args: {} } })) + socket?.send(JSON.stringify({ type: 'open', streamId: expiryStreamId, endpoint: 'account/watchExpiry', payload: { args: {} } })) + socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } })) + }) + socket.on('message', (data) => { + try { + const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data) + const frame = parseRemoteStreamServerMessage(bytes.toString('utf8')) + if (frame.streamId === analyticsPolicyStreamId) { + if (frame.type === 'item' && typeof frame.value === 'boolean') onAnalyticsEnabledChanged?.(frame.value) + else onAnalyticsEnabledChanged?.(false) + return + } + if (frame.streamId === expiryStreamId && frame.type === 'item' && frame.value === 'session-expired') { expired(); return } + if (frame.streamId !== streamId) throw new Error('desktop account: unexpected stream') + if (frame.type === 'item') listener(accountView(frame.value)) + else socket?.close() + } catch { socket?.close() } + }) + socket.on('error', () => { socket?.close() }) + socket.on('close', () => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } }) } connect() return () => { closed = true; onAnalyticsEnabledChanged?.(false); clearTimeout(retry); socket?.close() } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d8d0e30c4c..4c223c3c09 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -16,7 +16,6 @@ import { powerMonitor, nativeImage, nativeTheme, - net, protocol, session, shell, @@ -39,7 +38,7 @@ import { desktopUpdateChannel } from './update-channel.ts' import { DesktopUpdateCoordinator } from './update-coordinator.ts' import { prepareDesktopUpdateWindow } from './update-window.ts' import { DesktopCommandManager } from './command-management.ts' -import { serveWebDocument, authenticateWebHost, forwardWebRequest } from './web-document.ts' +import { serveWebDocument, authenticateWebHost, forwardWebRequest, removeLegacyHostCookies } from './web-document.ts' import { DesktopFatalRecovery } from './fatal-recovery.ts' import { pruneCrashReports, RendererConsoleTail, writeCrashReport, type CrashReportSource } from './crash-report.ts' import { OperationJournal } from './operation-journal.ts' @@ -529,11 +528,14 @@ async function main(): Promise { return { start: async () => { const ready = await host.start() - hostCookie = await authenticateWebHost(ready.url) + const cookie = await authenticateWebHost(ready.url) + hostCookie = cookie hostUrl = ready.url + void removeLegacyHostCookies(session.defaultSession.cookies, ready.url) + .catch((error: unknown) => { console.error('desktop: legacy Host cookie cleanup failed', error) }) if (ready.injections === undefined) throw new Error('Desktop Host did not provide boot injections') injections = ready.injections - welcomeBackend = await connectDesktopWelcome(ready.url, (input, init) => net.fetch(input, init), async () => (await session.defaultSession.cookies.get({ url: ready.url })).map(cookie => `${cookie.name}=${cookie.value}`).join('; ')) + welcomeBackend = connectDesktopWelcome(ready.url, cookie) analyticsEnabled = await welcomeBackend.analyticsEnabled().catch(() => false) if (!reportedLaunch) { reportedLaunch = true; void track('desktop_app_launch', {}) } stopAccount?.() diff --git a/apps/desktop/src/web-document.ts b/apps/desktop/src/web-document.ts index a74ea7c7ef..15f289e14b 100644 --- a/apps/desktop/src/web-document.ts +++ b/apps/desktop/src/web-document.ts @@ -49,6 +49,26 @@ export async function authenticateWebHost(url: string): Promise { return end < 0 ? cookie : cookie.slice(0, end) } +/** Cookie store operations used to remove legacy Host authentication cookies. */ +export interface HostCookieStore { + get(filter: { url: string }): Promise + remove(url: string, name: string): Promise +} + +/** + * Remove every `dsh-auth-*` cookie that earlier releases left in a cookie store for the Host's hostname. + * Cookies ignore ports, so cookies from earlier random-port Hosts would otherwise travel with every + * request to that hostname until they expire. + * @param cookies - Electron session cookie store. + * @param url - Host URL; cookies for its hostname on every port are removed. + */ +export async function removeLegacyHostCookies(cookies: HostCookieStore, url: string): Promise { + const origin = new URL(url).origin + for (const { name } of await cookies.get({ url: origin })) { + if (name.startsWith('dsh-auth-')) await cookies.remove(origin, name) + } +} + /** * Response headers not relayed to the renderer. `set-cookie` would hand the * Host's authentication cookie to the page's cookie jar, which the shell owns diff --git a/apps/desktop/src/welcome-backend.ts b/apps/desktop/src/welcome-backend.ts index 54ab46d85e..15b0932cb2 100644 --- a/apps/desktop/src/welcome-backend.ts +++ b/apps/desktop/src/welcome-backend.ts @@ -35,20 +35,18 @@ function record(value: unknown): value is Record { } /** - * Authenticate the native HTTP client through the Web application's launch URL. - * @param authenticatedUrl - URL supplied by the running Desktop Host. - * @param send - Electron session fetch, retaining the Web authentication cookie. + * Connect the native HTTP client to the Desktop Host; every request sends `cookie` explicitly and uses no cookie jar. + * @param hostUrl - URL of the running Desktop Host; only its origin is used. + * @param cookie - Cookie header returned by `authenticateWebHost` for this Host. + * @param send - HTTP client for Host requests. * @returns metadata reads and write-only credential operations over standard RPC. */ -export async function connectDesktopWelcome( - authenticatedUrl: string, - send: (input: string, init?: RequestInit) => Promise, - cookies: () => Promise = () => Promise.resolve(''), -): Promise { - const origin = new URL(authenticatedUrl).origin - const authenticated = await send(authenticatedUrl, { credentials: 'include' }) - await authenticated.body?.cancel() - if (!authenticated.ok) throw new Error('desktop welcome: Web authentication failed') +export function connectDesktopWelcome( + hostUrl: string, + cookie: string, + send: (input: string, init?: RequestInit) => Promise = fetch, +): DesktopWelcomeBackend { + const origin = new URL(hostUrl).origin const invoke = async ( request: { namespace: string; method: string; args: Record }, signal?: AbortSignal, @@ -56,8 +54,8 @@ export async function connectDesktopWelcome( const rpcId = randomUUID() const method = `${request.namespace}/${request.method}` const response = await send(new URL(`/api/${method}`, origin).href, { - method: 'POST', credentials: 'include', redirect: 'error', ...(signal === undefined ? {} : { signal }), - headers: { 'content-type': 'application/json' }, + method: 'POST', redirect: 'error', ...(signal === undefined ? {} : { signal }), + headers: { 'content-type': 'application/json', cookie }, body: JSON.stringify({ type: 'client-request', rpcId, method, payload: { args: request.args } }), }) if (!response.ok) throw new Error('desktop welcome: Web request failed') @@ -68,7 +66,7 @@ export async function connectDesktopWelcome( } return envelope.result.value } - const account = desktopAccountBackend(origin, invoke, cookies) + const account = desktopAccountBackend(origin, invoke, cookie) const settingsAndReference = async () => { const settings = await invoke({ namespace: 'settings', method: 'describe', args: {} }) if (!record(settings) || !Array.isArray(settings.namespaces)) throw new Error('desktop welcome: missing settings namespaces') diff --git a/apps/desktop/tests/account-backend.spec.ts b/apps/desktop/tests/account-backend.spec.ts index c239336b22..ffdc59d340 100644 --- a/apps/desktop/tests/account-backend.spec.ts +++ b/apps/desktop/tests/account-backend.spec.ts @@ -1,4 +1,5 @@ import { once } from 'node:events' +import type { IncomingMessage } from 'node:http' import { WebSocketServer } from 'ws' import { expect, it, onTestFinished, vi } from 'vitest' import { accountView, desktopAccountBackend } from '../src/account-backend.ts' @@ -28,7 +29,7 @@ it('uses account Remote commands without returning additional wire fields', asyn const backend = desktopAccountBackend('http://127.0.0.1:1234', (request) => { requests.push(request) return Promise.resolve({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null, token: 'private' }) - }, () => Promise.resolve('')) + }, '') const client = { version: '1.2.3', locale: 'en', timezoneOffsetSeconds: 28_800 } expect(await backend.start(client)).toEqual({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null }) expect(await backend.signOut(client)).toMatchObject({ status: 'signed-out' }) @@ -59,12 +60,13 @@ it('receives live expiry separately from account snapshots', async () => { const snapshot = { status: 'signed-out', attempt: null, links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' } } const received = Promise.withResolvers() - const backend = desktopAccountBackend(`http://127.0.0.1:${address.port}`, () => Promise.resolve(snapshot), () => Promise.resolve('')) + const backend = desktopAccountBackend(`http://127.0.0.1:${address.port}`, () => Promise.resolve(snapshot), 'dsh-auth-fixture=signed') const stop = backend.watch( (value) => { expect(value).toEqual(snapshot); received.resolve(undefined) }, vi.fn(), expired, onAnalyticsEnabledChanged, ) onTestFinished(stop) - await connected + const [, upgrade] = await connected as [unknown, IncomingMessage] + expect(upgrade.headers.cookie).toBe('dsh-auth-fixture=signed') // Logical streams share the socket; observe their opening frames before sending data. const streams = new Map() const opened = Promise.withResolvers() diff --git a/apps/desktop/tests/cua-optional-package.spec.ts b/apps/desktop/tests/cua-optional-package.spec.ts index 85ef15f0ee..a6b42cdcfd 100644 --- a/apps/desktop/tests/cua-optional-package.spec.ts +++ b/apps/desktop/tests/cua-optional-package.spec.ts @@ -1,4 +1,4 @@ -/** Cold pnpm requests exercise on-demand CUA dependency/platform metadata with inert package contents. */ +/** Cold pnpm requests exercise shipped 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).not.toHaveProperty(bundleName) + expect(cli.dependencies).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).map(name => [name, version])) const manifests = [ - { name: cli.name, version }, + { name: cli.name, version, dependencies: { [bundleName]: 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((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-base', 'darwin', 'win32', 'linux', 'linux-unrestricted']) { + for (const target of ['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: { [target === 'darwin-base' ? String(cli.name) : bundleName]: version } })) + await writeFile(join(project, 'package.json'), JSON.stringify({ private: true, dependencies: { [String(cli.name)]: version } })) await writeFile(join(project, 'pnpm-workspace.yaml'), [ - 'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : target === 'darwin-base' ? 'darwin' : target}]`, + 'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : 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.includes(`/${bundleName}.tgz`)).toBe(target !== 'darwin-base') - expect(requests.includes('/@deepseek-ai/dsh-cua.tgz')).toBe(target !== 'darwin-base') + expect(requests).toContain(`/${bundleName}.tgz`) + expect(requests).toContain('/@deepseek-ai/dsh-cua.tgz') expect(requests.includes(`/${providerName}.tgz`), `${target}: ${JSON.stringify(requests)}\n${output}`) .toBe(target === 'darwin' || unrestricted) } diff --git a/apps/desktop/tests/logged-notarytool-exit.spec.ts b/apps/desktop/tests/logged-notarytool-exit.spec.ts index 8deaf4ece4..82e14f232d 100644 --- a/apps/desktop/tests/logged-notarytool-exit.spec.ts +++ b/apps/desktop/tests/logged-notarytool-exit.spec.ts @@ -1,5 +1,10 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' import { expect, it, vi } from 'vitest' import { runLoggedNotarytool } from '../scripts/logged-notarytool.mjs' +import { createPackagingRun } from '../scripts/packaging-run.mjs' const command = vi.hoisted(() => vi.fn<(...args: unknown[]) => Promise<{ stdout: string; stderr: string }>>()) vi.mock('node:child_process', async (importOriginal) => { @@ -28,6 +33,52 @@ it('does not treat failed upload output as a completed submission', async () => command.mockRejectedValueOnce(Object.assign(new Error('upload failed'), { code: 1, stdout: JSON.stringify({ id }), stderr: '', })) - await expect(runLoggedNotarytool(args)).rejects.toThrow('upload failed') + const work = runLoggedNotarytool(args) + await expect(work).rejects.toThrow('notarytool submit failed (code: 1') + await expect(work).rejects.toHaveProperty('cause.message', 'upload failed') expect(command).toHaveBeenCalledOnce() }) + +it.each([false, true])('prints the redacted Apple failure with a journal=%s and leaves stdout for protocol replies', async (recorded) => { + const root = await mkdtemp(join(tmpdir(), 'notary-cli-')) + const directory = recorded ? createPackagingRun(root, {}).directory : undefined + const savedArgv = process.argv + const savedExit = process.exitCode + const savedDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const stdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) + try { + if (directory === undefined) delete process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = directory + process.argv = [process.execPath, fileURLToPath(new URL('../scripts/logged-notarytool.mjs', import.meta.url)), + 'submit', '/fixture/app.zip', '--apple-id', 'private@example.test', '--password', 'secret-sentinel', + '--team-id', 'private-team', '--wait', '--output-format', 'json'] + command.mockReset() + command.mockRejectedValueOnce(Object.assign(new Error('connection closed: private@example.test secret-sentinel private-team'), { + code: 69, signal: null, stdout: 'Apple response: secret-sentinel', stderr: 'HTTP 503 Service Unavailable', + })) + vi.resetModules() + await import('../scripts/logged-notarytool.mjs') + expect(command).toHaveBeenCalledOnce() + expect(process.exitCode).toBe(1) + expect(stdout).not.toHaveBeenCalled() + const output = stderr.mock.calls.map(([chunk]) => String(chunk)).join('') + const diagnostics = [output, ...directory === undefined ? [] : [await readFile(join(directory, 'events.jsonl'), 'utf8')]] + for (const diagnostic of diagnostics) { + expect(diagnostic).toContain('code: 69') + expect(diagnostic).toContain('Apple response: [REDACTED]') + expect(diagnostic).toContain('HTTP 503 Service Unavailable') + expect(diagnostic).toContain('connection closed') + for (const secret of ['private@example.test', 'secret-sentinel', 'private-team']) expect(diagnostic).not.toContain(secret) + } + expect(output.includes('events.jsonl')).toBe(recorded) + } finally { + process.argv = savedArgv + process.exitCode = savedExit + if (savedDirectory === undefined) delete process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = savedDirectory + stderr.mockRestore() + stdout.mockRestore() + await rm(root, { recursive: true, force: true }) + } +}) diff --git a/apps/desktop/tests/macos-runtime.spec.ts b/apps/desktop/tests/macos-runtime.spec.ts index 4ad63e41fd..b5ac451827 100644 --- a/apps/desktop/tests/macos-runtime.spec.ts +++ b/apps/desktop/tests/macos-runtime.spec.ts @@ -3,9 +3,11 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it, vi } from 'vitest' import { signMacOSRuntime } from '../scripts/macos-runtime.ts' -import { signMacOSRuntimeCode, verifyMacOSRuntimeCode } from '../scripts/verify-macos-signature.mjs' +import { signMacOSRuntimeCode, verifyMacOSNotarizedApplication, verifyMacOSRuntimeCode } from '../scripts/verify-macos-signature.mjs' -vi.mock('../scripts/verify-macos-signature.mjs', () => ({ signMacOSRuntimeCode: vi.fn(), verifyMacOSRuntimeCode: vi.fn() })) +vi.mock('../scripts/verify-macos-signature.mjs', () => ({ + signMacOSRuntimeCode: vi.fn(), verifyMacOSNotarizedApplication: vi.fn(), verifyMacOSRuntimeCode: vi.fn(), +})) const roots: string[] = [] function root(): string { const path = mkdtempSync(join(tmpdir(), 'desktop-signing-')) @@ -26,6 +28,34 @@ it('signs Mach-O files in their final locations and verifies each signature', as expect(verifyMacOSRuntimeCode).toHaveBeenCalledWith(join(path, 'addon.node'), identity) }) +const helperBundle = 'node_modules/@deepseek-ai/dsh-experimental-cua-macos/native/bin/darwin-arm64/generation/DSH Computer Use.app' +function machO(path: string): void { + mkdirSync(join(path, '..'), { recursive: true }) + writeFileSync(path, Buffer.from('cffaedfe00000000', 'hex')) +} + +it('verifies the notarized Computer Use helper as a bundle and signs other bundles per file', async () => { + const path = root() + machO(join(path, helperBundle, 'Contents/MacOS/dsh-cua-native')) + writeFileSync(join(path, helperBundle, 'Contents/Info.plist'), '') + // LibreOffice ships an unsigned application bundle whose executables are signed in place. + const office = join(path, 'node_modules/@deepseek-ai/libreoffice-kit-darwin-arm64/program/LibreOfficeDev.app/Contents/MacOS/soffice') + machO(office) + machO(join(path, 'addon.node')) + await expect(signMacOSRuntime(path, 'com.example.app', identity, 'arm64')).resolves.toBe(2) + expect(verifyMacOSNotarizedApplication).toHaveBeenCalledExactlyOnceWith(join(path, helperBundle), identity) + expect(vi.mocked(signMacOSRuntimeCode).mock.calls.map(([file]) => file).sort()).toEqual([join(path, 'addon.node'), office].sort()) +}) + +it('rejects a helper bundle that fails notarized-application verification before signing any file', async () => { + const path = root() + machO(join(path, helperBundle, 'Contents/MacOS/dsh-cua-native')) + machO(join(path, 'addon.node')) + vi.mocked(verifyMacOSNotarizedApplication).mockImplementationOnce(() => { throw new Error('stapler validate exited with 65') }) + await expect(signMacOSRuntime(path, 'com.example.app', identity, 'arm64')).rejects.toThrow('stapler validate') + expect(signMacOSRuntimeCode).not.toHaveBeenCalled() +}) + // File symlinks in these macOS fixtures require privileges unavailable to ordinary Windows processes. it.skipIf(process.platform === 'win32')('signs a shared Python interpreter once and retains both executable aliases', async () => { const path = root() diff --git a/apps/desktop/tests/macos-signature.spec.ts b/apps/desktop/tests/macos-signature.spec.ts index 2e6a2bd48e..861521546d 100644 --- a/apps/desktop/tests/macos-signature.spec.ts +++ b/apps/desktop/tests/macos-signature.spec.ts @@ -73,6 +73,7 @@ describe('desktop macOS release signature', () => { expect(config.asarUnpack).toEqual(expect.arrayContaining([ '**/*.{node,dylib,dll,so,exe}', '**/@vscode/ripgrep-*/bin/rg', + '**/node_modules/@deepseek-ai/dsh-experimental-cua-macos/native/bin/**/*', ])) expect(config).toMatchObject({ appId: RELEASE_ENVIRONMENT.DSH_DESKTOP_APP_ID, diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 3b1e63e00c..924808eb0d 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -22,7 +22,10 @@ type InvokeHandler = (event: InvokeEvent, ...args: unknown[]) => unknown const fileDrag = vi.hoisted(() => ({ dispose: vi.fn<() => Promise>(), install: vi.fn() })) vi.mock('../src/file-drag.ts', () => ({ installDesktopFileDrag: () => { fileDrag.install(); return fileDrag.dispose } })) -vi.mock('../src/web-document.ts', () => ({ authenticateWebHost: async () => 'test-cookie', serveWebDocument: vi.fn(), forwardWebRequest: vi.fn() })) +vi.mock('../src/web-document.ts', () => ({ + authenticateWebHost: async () => 'test-cookie', serveWebDocument: vi.fn(), forwardWebRequest: vi.fn(), + removeLegacyHostCookies: harness.removeLegacyHostCookies, +})) vi.mock('../src/operation-report-job.ts', () => ({ runOperationReportJob: vi.fn(async () => ({ appended: false, timedOut: false, cancelled: false })) })) // Report persistence has its own unit tests; here it resolves within microtasks so the fatal // dialog never outlives the test that triggered it. @@ -215,7 +218,8 @@ const harness = await vi.hoisted(async () => { failWindow(error: Error) { windowFailure = error }, windows, views, hosts, handlers, app, FakeWindow, FakeView, FakeHost, powerMonitor, nativeTheme, analytics, trays, FakeTray, backgroundNotice, shellDialog, - menu, popup, socketHeaders: vi.fn(), loginShell, readLoginShell, updateCheck, updateDownload, updateInstall, + menu, popup, socketHeaders: vi.fn(), connectWelcome: vi.fn(), removeLegacyHostCookies: vi.fn(async () => {}), hostCookies: {}, + loginShell, readLoginShell, updateCheck, updateDownload, updateInstall, platformDispose, platformCloseAndWait, @@ -306,7 +310,6 @@ vi.mock('electron', () => ({ dialog: harness.dialog, shell: { openExternal: harness.openExternal }, nativeTheme: harness.nativeTheme, - net: { fetch: vi.fn() }, ipcMain: { on: harness.ipcOn, handle: (channel: string, handler: InvokeHandler) => { @@ -317,7 +320,7 @@ vi.mock('electron', () => ({ }, Menu: { setApplicationMenu: harness.menu.setApplicationMenu, buildFromTemplate: harness.menu }, session: { defaultSession: { - on: vi.fn(), + on: vi.fn(), cookies: harness.hostCookies, setPermissionCheckHandler: vi.fn(), setPermissionRequestHandler: vi.fn(), webRequest: { onBeforeSendHeaders: harness.socketHeaders }, } }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: harness.protocolHandle }, @@ -389,14 +392,17 @@ vi.mock('../src/platform-view.ts', async importOriginal => ({ }, })) vi.mock('../src/welcome-backend.ts', () => ({ - connectDesktopWelcome: async () => ({ - analyticsEnabled: async () => harness.analyticsEnabled, - report: harness.analytics, - readLocalePreference: async () => null, - read: async (): Promise => (await harness.hosts.at(-1)!.fetch()).json() as Promise, - save: async () => ({ ok: true }), - account: { watch: harness.watchAccount, state: async () => ({ status: 'signed-out', attempt: null }) }, - }), + connectDesktopWelcome: (...args: unknown[]) => { + harness.connectWelcome(...args) + return { + analyticsEnabled: async () => harness.analyticsEnabled, + report: harness.analytics, + readLocalePreference: async () => null, + read: async (): Promise => (await harness.hosts.at(-1)!.fetch()).json() as Promise, + save: async () => ({ ok: true }), + account: { watch: harness.watchAccount, state: async () => ({ status: 'signed-out', attempt: null }) }, + } + }, })) function configureTestAuthPopup(allow: boolean): void { @@ -1126,7 +1132,7 @@ describe('desktop main startup', () => { } finally { harness.app.name = originalName } }) - it.each(['http:', 'https:'] as const)('attaches %s Host socket credentials only to the owned application origin and window', async (protocol) => { + it.each(['http:', 'https:'] as const)('passes the %s Host cookie explicitly and attaches socket credentials only to the owned application origin and window', async (protocol) => { await import('../src/main.ts') await harness.preparing.promise harness.prepared.resolve() @@ -1134,6 +1140,8 @@ describe('desktop main startup', () => { harness.hosts[0]!.url = `${protocol}//127.0.0.1:3080/?token=test` harness.hosts[0]!.ready.resolve() await Promise.resolve(invoke(DESKTOP_IPC.boot)) + expect(harness.connectWelcome).toHaveBeenCalledWith(`${protocol}//127.0.0.1:3080/?token=test`, 'test-cookie') + expect(harness.removeLegacyHostCookies).toHaveBeenCalledWith(harness.hostCookies, `${protocol}//127.0.0.1:3080/?token=test`) const handler = harness.socketHeaders.mock.calls[0]![1] as ( details: { url: string; webContentsId: number; requestHeaders: Record }, callback: (result: unknown) => void, @@ -1155,6 +1163,19 @@ describe('desktop main startup', () => { expect(callback).toHaveBeenLastCalledWith({}) }) + it('logs a legacy Host cookie cleanup failure without blocking boot', async () => { + const failure = new Error('cookie store unavailable') + harness.removeLegacyHostCookies.mockRejectedValueOnce(failure) + await import('../src/main.ts') + await harness.preparing.promise + harness.prepared.resolve() + await harness.hostStarted.promise + harness.hosts[0]!.ready.resolve() + await Promise.resolve(invoke(DESKTOP_IPC.boot)) + await vi.waitFor(() => { expect(console.error).toHaveBeenCalledWith('desktop: legacy Host cookie cleanup failed', failure) }) + expect(harness.connectWelcome).toHaveBeenCalled() + }) + it('shares login key discovery with onboarding and rejects foreign renderers', async () => { await readyForUpdate() const window = harness.windows[0]! diff --git a/apps/desktop/tests/web-document.spec.ts b/apps/desktop/tests/web-document.spec.ts index 0fd9b591b1..6507712ef6 100644 --- a/apps/desktop/tests/web-document.spec.ts +++ b/apps/desktop/tests/web-document.spec.ts @@ -2,7 +2,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { authenticateWebHost, forwardWebRequest, serveWebDocument } from '../src/web-document.ts' +import { authenticateWebHost, forwardWebRequest, removeLegacyHostCookies, serveWebDocument } from '../src/web-document.ts' const roots: string[] = [] afterEach(async () => { @@ -86,3 +86,14 @@ it('refuses another page origin without forwarding its request', async () => { expect(response.status).toBe(403) expect(fetch).not.toHaveBeenCalled() }) + +it('removes only legacy Host authentication cookies for the Host hostname', async () => { + const removed: [string, string][] = [] + const cookies = { + get: vi.fn(async () => [{ name: 'dsh-auth-old' }, { name: 'other-app' }, { name: 'dsh-auth-older' }]), + remove: vi.fn(async (url: string, name: string) => { removed.push([url, name]) }), + } + await removeLegacyHostCookies(cookies, 'http://127.0.0.1:1234/?token=secret') + expect(cookies.get).toHaveBeenCalledExactlyOnceWith({ url: 'http://127.0.0.1:1234' }) + expect(removed).toEqual([['http://127.0.0.1:1234', 'dsh-auth-old'], ['http://127.0.0.1:1234', 'dsh-auth-older']]) +}) diff --git a/apps/desktop/tests/welcome-flow.e2e.ts b/apps/desktop/tests/welcome-flow.e2e.ts index ce1dc896b2..08347912ec 100644 --- a/apps/desktop/tests/welcome-flow.e2e.ts +++ b/apps/desktop/tests/welcome-flow.e2e.ts @@ -12,6 +12,7 @@ import { DesktopProjectManager } from '../src/project-manager.ts' import { resolveDesktopPaths } from '../src/paths.ts' import { desktopClientMetadata } from '../src/client-metadata.ts' import { connectDesktopWelcome, type DesktopWelcomeBackend } from '../src/welcome-backend.ts' +import { authenticateWebHost } from '../src/web-document.ts' import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts' import { prepareDevelopmentProject } from '../scripts/development-project.ts' @@ -129,17 +130,7 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => { host = new DesktopHostProcess(process.execPath, project, paths.profile) const { url } = await host.start() hostOrigin = new URL(url).origin - let cookie = '' - const send: typeof fetch = async (input, init) => { - const headers = new Headers(init?.headers) - if (cookie !== '') headers.set('cookie', cookie) - const response = await fetch(input, { ...init, headers, redirect: 'manual' }) - if (response.status !== 303) return response - cookie = response.headers.getSetCookie().map(value => value.split(';')[0]).join('; ') - await response.body?.cancel() - return fetch(new URL(response.headers.get('location')!, url), { headers: { cookie } }) - } - backend = await connectDesktopWelcome(url, send, () => Promise.resolve(cookie)) + backend = connectDesktopWelcome(url, await authenticateWebHost(url)) } const status = async () => backend.read() const fingerprint = (): string => createHash('sha256') diff --git a/apps/desktop/tests/welcome-host.spec.ts b/apps/desktop/tests/welcome-host.spec.ts index dea2905583..d5dfb480a2 100644 --- a/apps/desktop/tests/welcome-host.spec.ts +++ b/apps/desktop/tests/welcome-host.spec.ts @@ -9,9 +9,8 @@ function transport(preference?: string) { { ns: 'llm-pi-ai', value: { profiles: { example: { apiKeyEnv: 'EXAMPLE_API_KEY' } } } }, { ns: 'locale', value: preference === undefined ? {} : { preference } }, ] - const send = vi.fn[1]>(async (_input, init) => { - if (init?.method !== 'POST') return new Response('index') - const { rpcId, method, payload } = JSON.parse(init.body as string) as { + const send = vi.fn[2]>>(async (_input, init) => { + const { rpcId, method, payload } = JSON.parse(init!.body as string) as { rpcId: string method: string payload: { args: { ref: string; value: string; refs: string[] } } @@ -29,27 +28,28 @@ function transport(preference?: string) { return { send, keys, namespaces } } -const url = 'http://127.0.0.1:19387/?token=fixture' +const url = 'http://127.0.0.1:19387/' +const cookie = 'dsh-auth-fixture=signed' describe('desktop welcome Web operations', () => { - it('authenticates through Web and stores only through the configured credential reference', async () => { + it('authenticates with the explicit Host cookie and stores only through the configured credential reference', async () => { const host = transport() - const backend = await connectDesktopWelcome(url, host.send) - expect(host.send).toHaveBeenCalledExactlyOnceWith(url, { credentials: 'include' }) + const backend = connectDesktopWelcome(url, cookie, host.send) expect(await backend.analyticsEnabled()).toBe(true) expect(await backend.save('sk-example')).toEqual({ ok: true }) expect(host.keys.get('CUSTOM_DEEPSEEK_KEY')).toBe('sk-example') expect(await backend.read()).toEqual({ loggedIn: false, hasApiKey: true, writable: true, localePreference: null }) - for (const [input, init] of host.send.mock.calls.slice(1)) { + for (const [input, init] of host.send.mock.calls) { expect(input).toMatch(/^http:\/\/127\.0\.0\.1:19387\/api\//u) - expect(init).toMatchObject({ credentials: 'include', redirect: 'error' }) + expect(init).toMatchObject({ redirect: 'error', headers: { cookie } }) + expect(init).not.toHaveProperty('credentials') } }) it('reads the explicit language preference and recognizes another provider key', async () => { const host = transport('zh') host.keys.set('EXAMPLE_API_KEY', 'sk-other') - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) expect(await backend.read()).toMatchObject({ localePreference: 'zh', hasApiKey: true }) host.keys.clear() expect(await backend.read()).toMatchObject({ hasApiKey: false }) @@ -58,7 +58,7 @@ describe('desktop welcome Web operations', () => { it('reads language without querying account or model providers', async () => { const host = transport('zh') - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) host.send.mockClear() expect(await backend.readLocalePreference()).toBe('zh') expect(host.send).toHaveBeenCalledOnce() @@ -68,7 +68,7 @@ describe('desktop welcome Web operations', () => { it('allows profiles without the official provider and retains custom key detection', async () => { const host = transport() host.namespaces.splice(0, 1) - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) expect(await backend.read()).toMatchObject({ hasApiKey: false, writable: false }) host.keys.set('EXAMPLE_API_KEY', 'custom-key') expect(await backend.read()).toMatchObject({ hasApiKey: true, writable: false }) @@ -78,7 +78,7 @@ describe('desktop welcome Web operations', () => { it.each(['', 'bad key', 'key\n'])('rejects malformed keys without contacting a provider: %s', async (value) => { const host = transport() - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) host.send.mockClear() expect(await backend.save(value)).toEqual({ ok: false }) expect(host.send).not.toHaveBeenCalled() @@ -86,28 +86,22 @@ describe('desktop welcome Web operations', () => { it('keeps provider diagnostics out of failures and rejects unmatched RPC envelopes', async () => { const host = transport() - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) host.send.mockRejectedValueOnce(new Error('private credential sk-must-not-leak')) expect(await backend.save('sk-example')).toEqual({ ok: false }) host.send.mockResolvedValueOnce(Response.json({ type: 'server-response', rpcId: 'other', result: { ok: true } })) await expect(backend.read()).rejects.toThrow('Web RPC failed') }) - it('submits native analytics through authenticated RPC with a bounded request', async () => { const host = transport() - const backend = await connectDesktopWelcome(url, host.send) + const backend = connectDesktopWelcome(url, cookie, host.send) const event = { eventName: 'desktop_app_launch' as const, timestamp: 100, attributes: {} } await backend.report(event) const [input, init] = host.send.mock.calls.at(-1)! expect(input).toBe('http://127.0.0.1:19387/api/productAnalytics/report') - expect(init?.credentials).toBe('include') + expect(init?.headers).toMatchObject({ cookie }) expect(init?.signal).toBeInstanceOf(AbortSignal) expect(JSON.parse(init!.body as string)).toMatchObject({ payload: { args: { event } } }) }) - - it('refuses an unauthenticated Web launch', async () => { - const send = vi.fn[1]>(async () => new Response(null, { status: 401 })) - await expect(connectDesktopWelcome(url, send)).rejects.toThrow('Web authentication failed') - }) }) diff --git a/apps/desktop/tests/welcome-startup.spec.ts b/apps/desktop/tests/welcome-startup.spec.ts index 4ae8743e2e..e578c8a655 100644 --- a/apps/desktop/tests/welcome-startup.spec.ts +++ b/apps/desktop/tests/welcome-startup.spec.ts @@ -1,4 +1,6 @@ -vi.mock('../src/web-document.ts', () => ({ authenticateWebHost: async () => 'test-cookie', serveWebDocument: vi.fn(), forwardWebRequest: vi.fn() })) +vi.mock('../src/web-document.ts', () => ({ + authenticateWebHost: async () => 'test-cookie', serveWebDocument: vi.fn(), forwardWebRequest: vi.fn(), removeLegacyHostCookies: async () => {}, +})) /** Welcome startup uses the Host before transitioning to the workspace. */ import { afterEach, expect, it, vi } from 'vitest' @@ -84,10 +86,9 @@ vi.mock('electron', () => ({ showInactive = state.showInactiveWorkspace async loadURL(url: string) { state.contents = this.webContents; await state.loadWorkspace(url); this.ready?.() } }, - net: { fetch: vi.fn() }, nativeTheme: state.nativeTheme, session: { defaultSession: { - on: vi.fn(), + on: vi.fn(), cookies: {}, setPermissionCheckHandler: vi.fn(), setPermissionRequestHandler: vi.fn(), webRequest: { onBeforeSendHeaders: vi.fn() }, } }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn() }, @@ -124,7 +125,7 @@ vi.mock('../src/host-process.ts', () => ({ }, })) vi.mock('../src/welcome-backend.ts', () => ({ - connectDesktopWelcome: async () => ({ + connectDesktopWelcome: () => ({ analyticsEnabled: async () => false, readLocalePreference: async () => state.preference, read: async () => { diff --git a/apps/web/tests/bundle-platform.e2e.ts b/apps/web/tests/bundle-platform.e2e.ts index 55d22be94d..2f3c673a3c 100644 --- a/apps/web/tests/bundle-platform.e2e.ts +++ b/apps/web/tests/bundle-platform.e2e.ts @@ -44,7 +44,7 @@ it('disables unsupported bundles, shows the Host restriction, and refuses direct const names = [name] await page.locator('[data-plugin-panel]').getByRole('button', { name: '更多', exact: true }).click() const cua = page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]') - expect(await cua.count()).toBe(0) + expect(await cua.count()).toBe(process.platform === 'darwin' ? 1 : 0) if (process.platform !== 'darwin') { expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle')) .toMatchObject({ listedByDefault: false, enabled: false, error: { code: 'unsupported-platform' } }) diff --git a/apps/web/tests/cua-optin.e2e.ts b/apps/web/tests/cua-optin.e2e.ts index d1962da38b..77ec920da5 100644 --- a/apps/web/tests/cua-optin.e2e.ts +++ b/apps/web/tests/cua-optin.e2e.ts @@ -53,22 +53,12 @@ it.skipIf(process.platform !== 'darwin').each([ await plugins.getByRole('button', { name: '更多', exact: true }).waitFor() expect(await page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').count()).toBe(0) await plugins.getByRole('button', { name: '更多', exact: true }).click() + // The installation ships the bundle off; the Experimental plugins list offers it without an installation step. const card = page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]') - expect(await card.count()).toBe(0) - expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle')) - .toMatchObject({ listedByDefault: false, enabled: false, installed: false }) - await plugins.getByRole('button', { name: '返回插件', exact: true }).click() - const offerCua = async () => { - await plugins.getByRole('button', { name: '添加插件', exact: true }).click() - const dialog = page.getByRole('dialog', { name: '添加插件', exact: true }) - await dialog.getByRole('textbox', { name: '包名或地址', exact: true }).fill('@deepseek-ai/dsh-cua-bundle') - await dialog.getByRole('button', { name: '安装', exact: true }).click() - const ready = page.getByRole('dialog', { name: '已安装', exact: true }) - await ready.waitFor() - expect(await ready.getByText('无需下载,启用即可使用', { exact: true }).count()).toBe(0) - return ready.getByRole('button', { name: '立即启用', exact: true }) - } - const enableNow = await offerCua() + await plugins.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').waitFor() + const listed = (await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle') + expect(listed).toMatchObject({ availability: 'installation', optional: true, enabled: false, installed: false }) + expect(listed).not.toHaveProperty('listedByDefault') let toggle = card.getByRole('switch', { name: '启用 Computer Use', exact: true }) const originalAgent = scaffold.ctx.agents.list()[0]! const ptcAgent = (await scaffold.ctx.agents.create({ @@ -97,12 +87,10 @@ it.skipIf(process.platform !== 'darwin').each([ return applying }) try { - await enableNow.click() - await expect.poll(() => enableNow.isEnabled()).toBe(false) - expect(await enableNow.getAttribute('aria-busy')).toBe('true') - expect(await card.count()).toBe(0) + await toggle.click() + await card.getByRole('status').getByText('正在应用 Computer Use 的更改', { exact: true }).waitFor() await compareOrRefreshGolden(fileURLToPath(new URL('./expected/cua-permissions/enabling.expected.md', import.meta.url)), - await captureStableAria(page, '[data-install-phase="done"] button[aria-busy]', scaffold.workspaceCwd), webSnapshotMode()) + await captureStableAria(page, '[data-plugin-package="@deepseek-ai/dsh-cua-bundle"] [role="status"]', scaffold.workspaceCwd), webSnapshotMode()) } finally { enabling.resolve(undefined) enableCall.mockRestore() diff --git a/apps/web/tests/cua-permissions.e2e.ts b/apps/web/tests/cua-permissions.e2e.ts index 68925fc144..d4c872ac7b 100644 --- a/apps/web/tests/cua-permissions.e2e.ts +++ b/apps/web/tests/cua-permissions.e2e.ts @@ -42,7 +42,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr }) resources.scaffold = scaffold expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle')) - .toMatchObject({ listedByDefault: false, enabled: true, installed: true }) + .toMatchObject({ availability: 'installation', enabled: true, installed: true }) const browser = await chromium.launch() resources.browser = browser const page = await newEnglishPage(browser), tripwire = watchConsole(page) diff --git a/apps/web/tests/desktop-onboarding.e2e.ts b/apps/web/tests/desktop-onboarding.e2e.ts index 24c3ada01e..417edf8bba 100644 --- a/apps/web/tests/desktop-onboarding.e2e.ts +++ b/apps/web/tests/desktop-onboarding.e2e.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { connectDesktopWelcome } from '../../desktop/src/welcome-backend.ts' +import { authenticateWebHost } from '../../desktop/src/web-document.ts' import { chromium, type Browser, type Page } from 'playwright' import { afterEach, beforeEach, describe, expect, it, onTestFailed } from 'vitest' import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials' @@ -107,7 +108,7 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () => async function desktopPage(locale = ZH_BROWSER_LOCALE): Promise { const opened = await browser.newPage({ viewport: { width: 1280, height: 840 }, locale, reducedMotion: 'reduce' }) - const welcome = await connectDesktopWelcome(scaffold.authenticatedUrl, (input, init) => scaffold.hostFetch(input, init)) + const welcome = connectDesktopWelcome(scaffold.baseUrl, await authenticateWebHost(scaffold.authenticatedUrl)) await opened.exposeFunction('__onboardingHasApiKey', async () => (await welcome.read()).hasApiKey) await opened.addInitScript(() => { Object.defineProperty(globalThis, 'dshOnboarding', { value: { diff --git a/apps/web/tests/expected/cua-permissions/enabling.expected.md b/apps/web/tests/expected/cua-permissions/enabling.expected.md index b9d6557e3e..44fc57e3e3 100644 --- a/apps/web/tests/expected/cua-permissions/enabling.expected.md +++ b/apps/web/tests/expected/cua-permissions/enabling.expected.md @@ -1 +1 @@ -- button "立即启用" [disabled] +- status: 正在应用 Computer Use 的更改 diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index 2787a31f3a..9bb7f1cfdd 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -855,9 +855,12 @@ describe('web e2e: plugin manager', () => { .toBe(bundles.filter(bundle => bundle.name !== '@deepseek-ai/dsh-experimental-terminal-bundle' && bundle.name !== '@deepseek-ai/dsh-experimental-liveshare' && bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length) + // Computer Use ships with the installation and is listed only on the macOS Hosts it supports. + const cuaSupported = process.platform === 'darwin' expect(bundles.find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle')) - .toMatchObject({ enabled: false, optional: false, availability: 'missing', listedByDefault: false }) - expect(await panel.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').count()).toBe(0) + .toMatchObject({ enabled: false, optional: true, availability: 'installation', ...cuaSupported ? {} : { listedByDefault: false } }) + expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').count()) + .toBe(cuaSupported ? 1 : 0) for (const name of ['@deepseek-ai/dsh-experimental-session-search', '@deepseek-ai/dsh-experimental-ralph-bundle']) { expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === name)) .toMatchObject({ enabled: false, optional: true, availability: 'installation', listedByDefault: false }) @@ -874,6 +877,7 @@ describe('web e2e: plugin manager', () => { ['@deepseek-ai/dsh-experimental-forge-bundle', 1], ['@deepseek-ai/dsh-personalized-recommendations', 1], ['@deepseek-ai/dsh-experimental-inspector-profile', 1], + ...cuaSupported ? [['@deepseek-ai/dsh-cua-bundle', 1]] : [], ['@deepseek-ai/dsh-experimental-tool-worktree', 1], ['@deepseek-ai/dsh-experimental-cot-translation-bundle', 1], ['@deepseek-ai/dsh-experimental-session-titles-bundle', 1], diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 3cf7695139..f1ea40b25c 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -694,7 +694,6 @@ async function toggleOptionalBundles(page: Page): Promise { }) } return { - cua: bundles.filter(entry => entry.name === '@deepseek-ai/dsh-cua-bundle'), optional: bundles.filter(entry => entry.optional).map(entry => ({ name: entry.name, enabled: entry.enabled, availability: entry.availability, error: entry.error ?? null, title: typeof entry.meta?.title, icon: typeof entry.meta?.icon, metaError: entry.meta?.error ?? null, @@ -706,16 +705,19 @@ async function toggleOptionalBundles(page: Page): Promise { }) expect(result.optional.map(entry => entry.name).sort()).toEqual([...OPTIONAL_BUNDLES].sort()) expect(result.optional.filter(entry => entry.enabled || entry.availability !== 'installation')).toEqual([]) - expect(result.optional.filter(entry => entry.error !== null)).toEqual([]) - expect(result.cua).toHaveLength(1) - expect(result.cua[0]).toMatchObject({ name: '@deepseek-ai/dsh-cua-bundle', optional: false, - availability: 'missing', error: { code: 'unsupported-platform' } }) + expect(result.optional.filter(entry => entry.error !== null)).toEqual([ + expect.objectContaining({ name: '@deepseek-ai/dsh-cua-bundle', error: { code: 'unsupported-platform' } }), + ]) // Metadata resolves through the Worker's Node internal modules: every optional bundle shows its title and icon. expect(result.optional.filter(entry => entry.metaError !== null || entry.title !== 'object' || entry.icon !== 'string')).toEqual([]) expect(result.inventoryErrors).toEqual([]) console.info(`preview boot: optional bundle toggles ${JSON.stringify(result.toggles.map(({ name, enable, disable, error }) => ({ name, enable, disable, error })))}`) - expect(result.toggles.find(toggle => toggle.name === '@deepseek-ai/dsh-cua-bundle')).toBeUndefined() - const supported = result.toggles + expect(result.toggles.find(toggle => toggle.name === '@deepseek-ai/dsh-cua-bundle')).toMatchObject({ + enable: 'failed', disable: 'applied', error: { code: 'unsupported-platform' }, + changed: 0, inactive: [], restored: true, + }) + // Computer Use refuses enablement on this non-macOS worker Host, as asserted above, so the generic check omits it. + const supported = result.toggles.filter(toggle => toggle.name !== '@deepseek-ai/dsh-cua-bundle') expect(supported.filter(toggle => toggle.enable !== 'applied' || toggle.disable !== 'applied') .map(({ name, enable, disable }) => ({ name, enable, disable }))).toEqual([]) // Each bundle changes Loader rows, every enabled row it changes runs, and switching it off restores the prior rows. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml index f8584a6842..7e4e4247a2 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml @@ -12,5 +12,5 @@ en: 1552866718133b10 zh: 1c760c5c2d5cfc0b /authorize-the-computer-use-helper/migration: - en: a79e5995415ff456 - zh: 6f818a092e53af2c + en: 2a28385e24468734 + zh: 725c1856a604bc0c diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md index c913488785..9e57c7c569 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md @@ -13,7 +13,7 @@ macOS Computer Use runs as `DSH Computer Use.app`, launched through LaunchServic ## Migration -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. +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. 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. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md index f25a4b805d..a6c3a2f634 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md @@ -13,7 +13,7 @@ macOS Computer Use 通过 LaunchServices 以 `DSH Computer Use.app` 运行。终 ## 迁移 -1. Computer Use 改为按需安装。在“插件 → 添加插件”中安装 `@deepseek-ai/dsh-cua-bundle` 并启用;此前选中过内置副本的 profile 也需完成这次安装。按提示重启 DSH。在 Computer Use 插件页按 macOS 提示,给 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。 +1. 升级 Computer Use 包后重启 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 沙箱策略。 diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index ceb82aac47..b1d696ad74 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -522,7 +522,6 @@ 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 } }) diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 207f34291f..8187c45a9b 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -1811,7 +1811,6 @@ it('lists absent Official entries offline without resolving their packages or co expect(bundles.filter(bundle => bundle.name === entry.packageName)).toMatchObject([{ 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() }, diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index 781b5d2a7c..4eea9e69b2 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: 07da57fb0a0cb10e - zh: c743fd96317bec22 + en: 5a06147f2a79eccf + zh: 6ca5899680e3cc6f /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: en: cd4e17d62c407541 zh: 9610d3ee55c0e56a diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 03ed8e8c24..5d71122e31 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -39,7 +39,7 @@ Installed bundles and their plugin rows display their own title and description A bundle's page shows the bundle's detail document first among its sections, without a heading of its own: the bundle's long description in the current UI language, as Markdown with images. It is the Markdown file that `meta.detail` in the bundle's `locale/.json` names, otherwise the bundle's README, read by [`pluginManager.readBundleDetail`](../../boot/plugin-manager/README.md#bundle-detail-documents) in the [document order of `dsh-plugin-detail`](../../util/plugin-detail/README.md#document-order); the repository a bundle declares is not read. An installed bundle's document comes from its own files without network access, and an on-demand Official bundle's from the npm file CDNs at its offered version. While the document loads the section shows three skeleton lines; a bundle without a document shows no section; a failed read names its reason with **Retry**. Raw HTML is omitted, relative images load from the Host beside the document, relative links stay text, and a document taller than the section's height limit scrolls inside it. A bundle whose files are not installed, such as an on-demand Official bundle, is read from the npm file CDNs: its read first asks the Host's registry comparison and, when npmmirror answered fastest, reads it and its images from npmmirror first. An installed bundle's read does not wait for the comparison. Any profile change and a connection reset read shown documents again. -On-demand Official cards follow package discovery visibility and are available offline before installation. In Add plugin, a bare Official package name uses the Host’s matching-version or source-checkout target, retains the selected registry, and offers Enable now after installation. Turning one On opens the existing install progress dialog, installs the [target offered by the running Host](../../boot/plugin-manager/README.md#use-this-package), and selects its layer. The same registry fallback, cancellation, and script-approval controls apply. Turning Off keeps the dependency installed; Remove is separate. A different version or development link source offers **Update**; details show current and offered sources and versions. An update removes the installed package and installs the offered one with the same selection, reporting whether a restart is required; when the installation fails after the removal, the package stays uninstalled. Missing or unreadable packages keep their recorded dependency source alongside the diagnostic. An enabled bundle can still be switched Off when its checkout target is unavailable. +On-demand Official cards are visible offline before installation. In Add plugin, a bare Official package name uses the Host’s matching-version or source-checkout target, retains the selected registry, and offers Enable now after installation. Turning one On opens the existing install progress dialog, installs the [target offered by the running Host](../../boot/plugin-manager/README.md#use-this-package), and selects its layer. The same registry fallback, cancellation, and script-approval controls apply. Turning Off keeps the dependency installed; Remove is separate. A different version or development link source offers **Update**; details show current and offered sources and versions. An update removes the installed package and installs the offered one with the same selection, reporting whether a restart is required; when the installation fails after the removal, the package stays uninstalled. Missing or unreadable packages keep their recorded dependency source alongside the diagnostic. An enabled bundle can still be switched Off when its checkout target is unavailable. The first list read shows a short group-heading placeholder and four skeleton cards, using the real cards' row, icon, and text-line spacing, with the action area left blank. They use the same opacity pulse as Session search and remain static with reduced motion. The accessible loading label and read timing are unchanged; refreshes retain the existing list, and installation and component-loading indicators keep their own states. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index b0dedeaad9..5978dc9d0e 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -39,7 +39,7 @@ kind: "package-reference" 组合包页面在其各部分中首先显示该组合包的详细介绍文档,不带单独的标题:以当前界面语言、带图片的 Markdown 显示该组合包的详细介绍。该文档是组合包 `locale/.json` 中 `meta.detail` 指定的 Markdown 文件,没有时为组合包的 README,由 [`pluginManager.readBundleDetail`](../../boot/plugin-manager/README.zh.md#bundle-detail-documents) 按 [`dsh-plugin-detail` 的文档顺序](../../util/plugin-detail/README.zh.md#document-order)读取;不读取组合包声明的仓库。已安装组合包的文档来自其自身文件,不访问网络;按需官方组合包的文档按其提供的版本从 npm 文件 CDN 读取。文档加载期间,该部分显示三行骨架屏;没有文档的组合包不显示该部分;读取失败时说明原因,并提供**重试**。原始 HTML 会被省略,相对路径的图片从 Host 上该文档旁的位置加载,相对链接保持为文本,文档高度超过该部分的高度上限时在该部分内滚动。文件未安装的组合包(例如按需官方组合包)从 npm 文件 CDN 读取:读取前先询问 Host 的注册表比较,npmmirror 响应最快时,先从 npmmirror 读取该文档及其图片。已安装组合包的读取不等待该比较。任何 profile 变化和连接重置都会重新读取已显示的文档。 -官方按需卡片遵循包的显示声明,在安装前即可离线读取。在“添加插件”中输入不带版本的官方包名时,使用 Host 提供的匹配版本或源码 checkout 目标,保留用户选择的安装源,并在安装后提供“立即启用”。开启会打开已有的安装进度对话框,安装[当前 Host 提供的目标](../../boot/plugin-manager/README.zh.md#use-this-package)并选择配置层。安装源回退、取消和脚本审批继续使用同一组控件。关闭保留依赖;移除是独立操作。版本或开发链接来源不同会提供**更新**;详情显示当前及所提供的来源和版本。更新先删除已安装的包,再以相同的选择状态安装所提供的包,并报告是否需要重启;删除之后安装失败时,该包保持未安装。文件缺失或不可读的包在诊断旁保留记录的依赖来源。已启用的 bundle 在 checkout 目标不可用时仍可关闭。 +官方按需卡片在安装前即可离线显示。在“添加插件”中输入不带版本的官方包名时,使用 Host 提供的匹配版本或源码 checkout 目标,保留用户选择的安装源,并在安装后提供“立即启用”。开启会打开已有的安装进度对话框,安装[当前 Host 提供的目标](../../boot/plugin-manager/README.zh.md#use-this-package)并选择配置层。安装源回退、取消和脚本审批继续使用同一组控件。关闭保留依赖;移除是独立操作。版本或开发链接来源不同会提供**更新**;详情显示当前及所提供的来源和版本。更新先删除已安装的包,再以相同的选择状态安装所提供的包,并报告是否需要重启;删除之后安装失败时,该包保持未安装。文件缺失或不可读的包在诊断旁保留记录的依赖来源。已启用的 bundle 在 checkout 目标不可用时仍可关闭。 首次读取列表时,页面显示一条较短的分组标题占位和四行骨架卡片,沿用真实卡片的行、图标和文字间距,操作区域留白。骨架使用与 Session 搜索相同的透明度呼吸动效,在减少动态效果模式下保持静止。无障碍加载文案和读取时机不变;刷新时保留已有列表,安装及组件加载仍使用各自的状态提示。 diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 242ca6f291..2f504fbc9e 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -1256,7 +1256,6 @@ describe('PluginManagerPage', () => { }) it.each([ - '@deepseek-ai/dsh-cua-bundle', '@deepseek-ai/dsh-experimental-session-search', '@deepseek-ai/dsh-experimental-ralph-bundle', '@acme/shipped-exploration', diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts index 67b95bded7..dd8cb347e7 100644 --- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts +++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts @@ -1979,10 +1979,10 @@ 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' } } +it.each(['registry', 'source'] as const)('installs a typed Official catalog name using its %s target', async (mode) => { + const name = CATALOG.name + const spec = mode === 'source' ? 'link:/workspace/subagent-codex' : `${name}@2.0.0` + const entry = { ...CATALOG, installTarget: { spec, version: '2.0.0' } } const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([entry])) }) await controller.load() face.openInstall() diff --git a/packages/experimental/cua-bundle/README.i18n.yaml b/packages/experimental/cua-bundle/README.i18n.yaml index b8f7ced3c7..b561f27e44 100644 --- a/packages/experimental/cua-bundle/README.i18n.yaml +++ b/packages/experimental/cua-bundle/README.i18n.yaml @@ -9,14 +9,14 @@ en: 9084fb48d0f6489c zh: ce692972d91cf663 /deepseek-ai-dsh-cua-bundle/summary: - en: 5f213491dd724c91 - zh: 228439436d3b339c + en: 0d93f44a8ce7404c + zh: 6a73ed42d0efc9ea /deepseek-ai-dsh-cua-bundle/table-of-contents: en: b563e4ec4727d0de zh: d0228da978238e05 /deepseek-ai-dsh-cua-bundle/use-this-package: - en: 92b549c20b7b8e63 - zh: 90df835c4e9bac75 + en: 5c025a58990bb2eb + zh: c3d1280ccbdfe214 /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: 7d91694e2f6b111e - zh: 54bcc0cef9496ed6 + en: 559776e398bb7c47 + zh: 27adfd2cacad1ee6 /deepseek-ai-dsh-cua-bundle/known-limitations-and-deferred-work/dev-note: en: 0a1253c6f7b5e8b9 zh: ce96645b3e75874e diff --git a/packages/experimental/cua-bundle/README.md b/packages/experimental/cua-bundle/README.md index 63bdd67b27..50109f1066 100644 --- a/packages/experimental/cua-bundle/README.md +++ b/packages/experimental/cua-bundle/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Enable macOS Computer Use from Plugins for existing and new PTC sessions. Windows support is in development. This bundle remains in `experimental` until that support is complete, then moves to the `computer-use` group. It is off by default and hidden from the default plugin lists until enabled or separately installed; disabled profiles prepare no native components and register neither CUA tools nor their model guidance. +Enable macOS Computer Use from Plugins for existing and new PTC sessions. Windows support is in development. This bundle remains in `experimental` until that support is complete, then moves to the `computer-use` group. It ships with DSH switched off and appears in the Experimental plugins list on macOS Hosts; disabled profiles prepare no native components and register neither CUA tools nor their model guidance. ## Table of Contents @@ -24,7 +24,7 @@ Enable macOS Computer Use from Plugins for existing and new PTC sessions. Window ## Use this package -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: +On macOS, open **Plugins → More** and switch on **Computer use** in the Experimental plugins list. The bundle ships with DSH and updates with it, so enabling it downloads nothing. Published packages contain the signed and notarized helper; users need no compiler. Source Web and development Desktop prepare an ad-hoc helper when enabled. Open its details, choose PTC if needed, and grant Accessibility and Screen Recording to `DSH Computer Use.app`. An enabled bundle also appears in the main plugin list; switching it off returns it to the Experimental plugins list only. For source Web, run these commands from the repository root: ```sh pnpm run build @@ -33,11 +33,11 @@ pnpm dsh web For the source Desktop launcher and its separate development profile, follow [Desktop development](../../../apps/desktop/README.md). Enable Computer Use in each profile that needs it. -The switch saves bundle selection in the profile’s `package.json`. Computer Use is installed only in PTC sessions. Enabling through Add plugin, a visible card, or its detail page offers setup guidance when system permissions need attention. The detail page offers **Set to PTC** for the new-session default; existing sessions keep their preset. The switch leaves PTC execution mode and other settings unchanged. The detail page’s **Use persistent mode** action separately enables in-memory state across PTC calls; its effects and reload behavior are described in the [permission page](../cua/README.md). Native preparation runs when the provider loads and reuses the source-hashed executable when available. +The switch saves bundle selection in the profile’s `package.json`. Computer Use is installed only in PTC sessions. Enabling through its card or detail page offers setup guidance when system permissions need attention. The detail page offers **Set to PTC** for the new-session default; existing sessions keep their preset. The switch leaves PTC execution mode and other settings unchanged. The detail page’s **Use persistent mode** action separately enables in-memory state across PTC calls; its effects and reload behavior are described in the [permission page](../cua/README.md). Native preparation runs when the provider loads and reuses the source-hashed executable when available. Disabling unloads the provider and withdraws CUA tools from its Agent scopes. Runtime configuration, the new-session default and saved grants remain. -Computer Use is available only on a macOS DSH Host. The native provider is an optional dependency installed only on macOS; the bundle and its shared libraries install on demand. Other Hosts hide the bundle from default plugin lists and model recommendations and refuse enablement before saving settings. Explicit profile dependencies and saved selections remain visible for management. An already selected bundle is skipped at startup with a diagnostic and remains available for deselection; saved user settings remain. A browser on another operating system can connect to a supported Mac Host. +Computer Use is available only on a macOS DSH Host. The native provider is an optional dependency installed only on macOS; shared CUA and Client packages remain in the installation. Other Hosts hide the bundle from default plugin lists and model recommendations and refuse enablement before saving settings. Explicit profile dependencies and saved selections remain visible for management. An already selected bundle is skipped at startup with a diagnostic and remains available for deselection; saved user settings remain. A browser on another operating system can connect to a supported Mac Host. If preparation fails, the saved selection remains and CUA operations stay unavailable. Refresh or reopen the plugin page to read the component diagnostic, correct its cause, then restart DSH. @@ -85,7 +85,7 @@ CUA activation and removal change the PTC SDK and system guidance; subsequent re -- 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. +- System permissions require the user’s approval in macOS; enabling the bundle does not grant them. - PTC execution is shared by the profile and is configured independently. A Host without HMR applies saved changes at restart. diff --git a/packages/experimental/cua-bundle/README.zh.md b/packages/experimental/cua-bundle/README.zh.md index e537ab287e..b7d8804e91 100644 --- a/packages/experimental/cua-bundle/README.zh.md +++ b/packages/experimental/cua-bundle/README.zh.md @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -从插件页为已有和新建 PTC 会话开启 macOS Computer Use。Windows 支持正在开发中。本组合包暂放在 `experimental`,待 Windows 支持完成后迁回 `computer-use` 组。默认关闭,启用或单独安装前不在默认插件列表中显示;关闭时不准备本机组件、不注册 CUA 工具或添加其模型指引。 +从插件页为已有和新建 PTC 会话开启 macOS Computer Use。Windows 支持正在开发中。本组合包暂放在 `experimental`,待 Windows 支持完成后迁回 `computer-use` 组。随 DSH 提供且默认关闭,在 macOS Host 的实验性插件列表中显示;关闭时不准备本机组件、不注册 CUA 工具或添加其模型指引。 ## 目录 @@ -24,7 +24,7 @@ kind: "package-bundle" ## 使用本包 -在 macOS Desktop 的“插件 → 添加插件”中输入 `@deepseek-ai/dsh-cua-bundle`,点击“安装”,再点击“立即启用”。DSH 将与当前运行时版本匹配的官方插件安装到 profile。发布包包含已签名和公证的 helper,用户无需编译工具。源码 Web 和开发 Desktop 链接工作区组合包,启用时准备临时签名的 helper。打开 Computer Use 详情,按需设置 PTC,并为 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。已安装的组合包关闭后仍可管理。源码 Web 在仓库根目录运行以下命令: +在 macOS 上打开“插件 → 更多”,在实验性插件列表中开启 Computer Use。组合包随 DSH 提供并随之升级,开启时无需下载。发布包包含已签名和公证的 helper,用户无需编译工具。源码 Web 和开发 Desktop 在开启时准备临时签名的 helper。打开它的详情,按需设置 PTC,并为 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。开启后组合包也会出现在插件主列表中,关闭后只留在实验性插件列表。源码 Web 在仓库根目录运行以下命令: ```sh pnpm run build @@ -33,11 +33,11 @@ pnpm dsh web 源码 Desktop 的启动方式和独立开发配置档见[桌面开发说明](../../../apps/desktop/README.zh.md)。需要使用 Computer Use 的配置档分别开启该功能。 -开关将组合包选择保存到 profile 的 `package.json`。Computer Use 仅安装在 PTC 会话中。通过「添加插件」、可见卡片或详情页启用时,系统权限未就绪会显示设置引导。详情页提供“设为 PTC”按钮来修改新会话默认模式,已有会话保留原 preset。开关保持 PTC 执行方式和其他设置不变。详情页的“设为持久化”按钮单独开启 PTC 跨调用的内存状态保留,作用范围和重载行为见[权限页面](../cua/README.zh.md)。提供方加载时准备本机组件,已有与源码摘要匹配的可执行文件时直接复用。 +开关将组合包选择保存到 profile 的 `package.json`。Computer Use 仅安装在 PTC 会话中。通过卡片或详情页启用时,系统权限未就绪会显示设置引导。详情页提供“设为 PTC”按钮来修改新会话默认模式,已有会话保留原 preset。开关保持 PTC 执行方式和其他设置不变。详情页的“设为持久化”按钮单独开启 PTC 跨调用的内存状态保留,作用范围和重载行为见[权限页面](../cua/README.zh.md)。提供方加载时准备本机组件,已有与源码摘要匹配的可执行文件时直接复用。 关闭会卸载提供方,并从其 Agent 作用域中撤回 CUA 工具。执行器配置、新会话默认值和已保存的授权保留。 -Computer Use 仅支持 macOS DSH Host。原生提供方是仅在 macOS 安装的可选依赖,组合包和共享库均按需安装。其他 Host 默认不在插件列表和模型推荐中展示此组合包,并在保存设置前拒绝启用。profile 显式安装的依赖和已保存的选择仍保留管理入口。已选中的组合包会在启动时跳过并报告原因,仍可取消选择;已保存的用户设置保留。其他操作系统上的浏览器可以连接受支持的 Mac Host。 +Computer Use 仅支持 macOS DSH Host。原生提供方是仅在 macOS 安装的可选依赖,共享 CUA 和 Client 包仍随安装提供。其他 Host 默认不在插件列表和模型推荐中展示此组合包,并在保存设置前拒绝启用。profile 显式安装的依赖和已保存的选择仍保留管理入口。已选中的组合包会在启动时跳过并报告原因,仍可取消选择;已保存的用户设置保留。其他操作系统上的浏览器可以连接受支持的 Mac Host。 准备失败时,已保存的选择保留,CUA 操作不可用。刷新或重新打开插件页查看组件诊断,修复后重启 DSH。 @@ -85,7 +85,7 @@ CUA 生效和卸载会改变 PTC SDK 与系统指引,后续请求使用更新 -- 首次安装需要能访问提供匹配 DSH 版本的注册表。系统权限需用户在 macOS 中确认,安装插件不会自动授予权限。 +- 系统权限需用户在 macOS 中确认,开启组合包不会自动授予权限。 - PTC 执行方式由 profile 共享,需单独配置。未启用 HMR 的 Host 在重启时应用已保存的变更。 diff --git a/packages/experimental/cua-bundle/package.json b/packages/experimental/cua-bundle/package.json index 604a02ea63..93612c3ffc 100644 --- a/packages/experimental/cua-bundle/package.json +++ b/packages/experimental/cua-bundle/package.json @@ -35,7 +35,6 @@ "license": "MIT", "dsh": { "bundle": { - "listedByDefault": false, "patch": "./cordis.patch.yml", "platforms": [ "darwin" diff --git a/packages/experimental/cua-macos/README.i18n.yaml b/packages/experimental/cua-macos/README.i18n.yaml index 0ed84d6b08..5ec1b9b13d 100644 --- a/packages/experimental/cua-macos/README.i18n.yaml +++ b/packages/experimental/cua-macos/README.i18n.yaml @@ -12,11 +12,11 @@ en: 4549f02fc53854a5 zh: 6f74f1130b6623a8 /deepseek-ai-dsh-experimental-cua-macos/table-of-contents: - en: 213d00372aa34638 - zh: 4a39a8e44b15b0f8 + en: 1777e7bf6785f986 + zh: e3daac1074ebb74d /deepseek-ai-dsh-experimental-cua-macos/use-this-package: - en: bc022ec8eddd9ae6 - zh: 3a583ea4f33c9c81 + en: a6006411c8240e1a + zh: 4c0129caa37b087d /deepseek-ai-dsh-experimental-cua-macos/use-this-package/connect-an-external-execution-environment: en: 786b8fd630b11e5f zh: 55143f15486afce8 diff --git a/packages/experimental/cua-macos/README.md b/packages/experimental/cua-macos/README.md index 0bc24469b8..3a4e0b47bb 100644 --- a/packages/experimental/cua-macos/README.md +++ b/packages/experimental/cua-macos/README.md @@ -22,7 +22,7 @@ Inspect macOS permissions, select running applications and windows, read accessi The helper supplies bounded 64-pixel PNG application icons for permission prompts and saved-application lists. Icon reads do not launch an application or inspect its windows; icons do not determine reusable consent. -The helper runs through LaunchServices as an independent application: `DSH Computer Use.app` for release builds and `DSH Computer Use Dev.app` for source builds. Accessibility and Screen Recording permissions belong to this application. Permission guidance uses the responsible application’s bundle URL, identifier, filename, and system icon captured at startup; unavailable system attribution falls back to the independent helper’s own bundle. Dragging and Finder selection read the current on-disk bundle identifier and executable through the application-discovery reader. The page and guide both display the application’s `.app` filename. Existing connections retain their running application generation when another build updates `current.json`. +The helper runs through LaunchServices as an independent application: `DSH Computer Use.app` for release builds and `DSH Computer Use Dev.app` for source builds. Accessibility and Screen Recording permissions belong to this application. Permission guidance uses the responsible application’s bundle URL, identifier, filename, and system icon captured at startup; unavailable system attribution falls back to the independent helper’s own bundle. Dragging and Finder selection read the current on-disk bundle identifier and executable through the application-discovery reader. The page and guide both display the application’s `.app` filename. Existing connections retain their running application generation when another build updates `current.json`. When bundled inside another `.app`, it starts from a verified standalone copy; see [application storage](native/README.md#build-and-service). 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. @@ -33,7 +33,7 @@ The provider resolves guide and recovery copy from the current Client language, ## Use this package -This package is an optional macOS dependency. For local development, run `pnpm --dir packages/experimental/cua-macos run build:native` to generate an ad-hoc-signed application and `current.json` under `native/bin`. Release packaging requires `--identity "Developer ID Application: ()"` and notarization; see [native build and service](native/README.md#build-and-service). Installed packages preserve the distributed signature. Source preparation requires Swift and Python 3 from Apple Command Line Tools. +This package is an optional macOS dependency. For local development, run `pnpm --dir packages/experimental/cua-macos run build:native` to generate an ad-hoc-signed application and `current.json` under `native/bin`. Release packaging requires `--identity "Developer ID Application: ()"` and notarization; see [native build and service](native/README.md#build-and-service). Installed packages preserve the distributed signature. A package inside an Electron `app.asar` archive reads its application from the sibling `app.asar.unpacked` directory, which the Desktop installer populates; an installed package without the application fails preparation with an error naming the expected directory. Source preparation requires Swift and Python 3 from Apple Command Line Tools. For repository Web use, enable [Computer Use](../cua-bundle/README.md) in Plugins. This prepares the local executable on macOS 14 or later; Windows support is in development. Subsequent builds check it while the existing Web profile keeps the bundle selected. Disabled profiles do not prepare native components. Explicit source composition is also available: diff --git a/packages/experimental/cua-macos/README.zh.md b/packages/experimental/cua-macos/README.zh.md index 7bce161fc3..6702dc959d 100644 --- a/packages/experimental/cua-macos/README.zh.md +++ b/packages/experimental/cua-macos/README.zh.md @@ -22,7 +22,7 @@ kind: "package-reference" helper 为授权提示和已授权应用列表提供 64 像素 PNG 图标。读取图标不会启动应用或检查窗口,图标不决定可复用授权。 -helper 通过 LaunchServices 作为独立应用运行,正式版为 `DSH Computer Use.app`,源码开发版为 `DSH Computer Use Dev.app`。辅助功能和屏幕录制权限属于此应用。授权引导使用启动时记录的责任应用 bundle URL、标识、文件名和系统图标;系统归属信息不可用时使用独立 helper 自身的 bundle。拖动和 Finder 选择通过应用发现模块读取磁盘上当前的 bundle 标识及可执行文件。页面和引导框统一显示应用的 `.app` 文件名。其他构建更新 `current.json` 后,已有连接仍指向正在运行的应用版本。 +helper 通过 LaunchServices 作为独立应用运行,正式版为 `DSH Computer Use.app`,源码开发版为 `DSH Computer Use Dev.app`。辅助功能和屏幕录制权限属于此应用。授权引导使用启动时记录的责任应用 bundle URL、标识、文件名和系统图标;系统归属信息不可用时使用独立 helper 自身的 bundle。拖动和 Finder 选择通过应用发现模块读取磁盘上当前的 bundle 标识及可执行文件。页面和引导框统一显示应用的 `.app` 文件名。其他构建更新 `current.json` 后,已有连接仍指向正在运行的应用版本。 helper 嵌套在其他 `.app` 中时,会从经过校验的独立副本启动,存放与校验规则见[原生构建与服务](native/README.zh.md#build-and-service)。 权限请求打开对应隐私设置页和原生悬浮引导框。引导框首次选择面积最大的设置窗口,随后按窗口 ID 跟随。它使用普通窗口层级,紧邻所选窗口上方排列,使认证弹窗保持在其上方。定位读取窗口边界,不申请辅助功能权限。引导框等到识别出设置窗口后才显示;打开设置失败时独立显示手动导航说明。材质的透明度遮罩为面板和阴影提供圆角。最新请求替换共享引导,连接清理只关闭自身创建的引导。应用不可用时仍尝试打开设置,并在页面报告失败原因。权限已授予时只打开设置;显式进入设置会关闭原有引导。系统拒绝打开设置时报告 `SETTINGS_UNAVAILABLE`;应用目标缺失或被替换时报告 `APPLICATION_UNAVAILABLE`。系统设置决定是否接收拖入的应用,以及是否要求重新启动。 @@ -33,7 +33,7 @@ provider 按当前界面语言生成引导和恢复文案,不支持的语言 ## 使用本包 -本包是仅限 macOS 的可选依赖。本地开发时运行 `pnpm --dir packages/experimental/cua-macos run build:native`,在 `native/bin` 下生成临时签名应用及 `current.json`。正式打包需要指定 `--identity "Developer ID Application: ()"` 并完成公证,参见[原生构建与服务](native/README.zh.md#build-and-service)。安装版保留分发时的签名。源码准备需要 Apple Command Line Tools 提供的 Swift 和 Python 3。 +本包是仅限 macOS 的可选依赖。本地开发时运行 `pnpm --dir packages/experimental/cua-macos run build:native`,在 `native/bin` 下生成临时签名应用及 `current.json`。正式打包需要指定 `--identity "Developer ID Application: ()"` 并完成公证,参见[原生构建与服务](native/README.zh.md#build-and-service)。安装版保留分发时的签名。位于 Electron `app.asar` 归档内的包从同级的 `app.asar.unpacked` 目录读取应用,该目录由 Desktop 安装包提供;已安装包缺少应用时,准备阶段报错并给出预期目录。源码准备需要 Apple Command Line Tools 提供的 Swift 和 Python 3。 从仓库运行 Web 时,在插件页开启 [Computer Use](../cua-bundle/README.zh.md)。首次启用会在 macOS 14 及以上准备本机组件,Windows 支持正在开发中。现有 Web profile 保持选中时,后续构建继续检查更新。未开启的 profile 不准备原生组件。也可以显式加载源码覆盖层: diff --git a/packages/experimental/cua-macos/native/README.i18n.yaml b/packages/experimental/cua-macos/native/README.i18n.yaml index ca8a9ff9ef..b18b049528 100644 --- a/packages/experimental/cua-macos/native/README.i18n.yaml +++ b/packages/experimental/cua-macos/native/README.i18n.yaml @@ -15,8 +15,8 @@ en: e8cbc43d38786c40 zh: 52b3ba85d98011b9 /dsh-cua-native-helper/build-and-service: - en: 5527fc276554cb68 - zh: ef73a54c24019d79 + en: afdfde0453130386 + zh: 909ca94305262e14 /dsh-cua-native-helper/request-protocol: en: fc8043c425322cdd zh: cf1edb9a78fda301 @@ -24,8 +24,8 @@ en: cec90793e80d1aa6 zh: 766da7ec48a65ad1 /dsh-cua-native-helper/verification: - en: 32d832fd7a9a60a7 - zh: e80b5e47fd60cc8f + en: 972463214e3d766b + zh: 4c7aaf91ed257d5b /dsh-cua-native-helper/further-exploration: en: 265e1927952ce6d7 zh: 7c35d9fd98b6e6c8 diff --git a/packages/experimental/cua-macos/native/README.md b/packages/experimental/cua-macos/native/README.md index c141a76f9b..0d42c3905c 100644 --- a/packages/experimental/cua-macos/native/README.md +++ b/packages/experimental/cua-macos/native/README.md @@ -23,7 +23,7 @@ This macOS 14+ JSON-lines helper uses Swift 6, AppKit, Accessibility, CoreGraphi ## Build and service -Run `pnpm --dir packages/experimental/cua-macos run build:native` to publish a signed `DSH Computer Use.app` under `native/bin/darwin-`. The `current.json` receipt selects a complete content-addressed generation. Signing defaults to ad hoc; release packagers can pass `--identity` for Developer ID signing and must notarize the distribution. The Host runs the application executable with `--ensure-shared`, which launches the background service through LaunchServices. The service exits after its last connection finishes cleanup. +Run `pnpm --dir packages/experimental/cua-macos run build:native` to publish a signed `DSH Computer Use.app` under `native/bin/darwin-`. The `current.json` receipt selects a complete content-addressed generation. Signing defaults to ad hoc; release packagers can pass `--identity` for Developer ID signing and must notarize the distribution. The Host runs the application executable with `--ensure-shared`, which launches the background service through LaunchServices. The service exits after its last connection finishes cleanup. When the helper is nested inside another `.app`, its launcher copies the complete signed bundle to `~/Library/Application Support/DeepSeek Harness/Computer Use//` before starting it. Each immutable generation is verified against the executable hash and application signature; earlier generations remain available for running installations. This keeps both macOS permission services attributed to the helper. A failed verification stops startup; remove the named cached generation before retrying. Source checkouts build with `--variant dev`, producing `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Development and release builds default to ad-hoc signing. Ad-hoc requirements contain the code hash, so changed binaries can require new grants. @@ -37,7 +37,7 @@ Certificate signatures include a signing time. Development builds record the gen Generations in `native/.build/prepared/` and `native/bin/darwin-` 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). Packaging does not require a macOS desktop login or Rosetta. `pnpm exec tsx scripts/cua-helper/package.ts --out ` runs the same preparation from the repository root. It signs, notarizes and staples each complete application before selecting its final content digest, then verifies architectures, signatures, tickets, Gatekeeper and file integrity from the extracted `cua-helper.tgz`. Packaging does not launch the helper. Run `python3 scripts/cua-helper/smoke.py ` separately in a logged-in desktop session to check capabilities, helper identity and cleanup; x64 execution on Apple Silicon requires Rosetta. The smoke does not request permissions or inspect applications. Failed or timed-out commands and unreadable startup replies report captured stdout and stderr; invalid encoding is replaced only in diagnostic text, while protocol parsing remains strict. A successful package build does not certify runtime behavior. +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). Packaging does not require a macOS desktop login or Rosetta. `pnpm exec tsx scripts/cua-helper/package.ts --out ` runs the same preparation from the repository root. It signs, notarizes and staples each complete application before selecting its final content digest, then verifies architectures, signatures, tickets, Gatekeeper and file integrity from the extracted `cua-helper.tgz`. Packaging does not launch the helper. Run `python3 scripts/cua-helper/smoke.py ` separately in a logged-in desktop session to check capabilities, helper identity and cleanup; x64 execution on Apple Silicon requires Rosetta. The smoke does not request permissions or inspect applications. Failed or timed-out commands and unreadable startup replies report captured stdout and stderr; invalid encoding is replaced only in diagnostic text, while protocol parsing remains strict. A successful package build does not certify runtime behavior. Helper packaging retains its own [diagnostic record](../../../../apps/desktop/packaging/README.md#macos-release-signing), including failures before desktop installer startup. 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. @@ -135,7 +135,7 @@ Run `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py - Run `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper` in an unlocked macOS GUI session with the Apple Swift compiler available. It owns and cleans up its AppKit bundles and standalone GUI processes, checks metadata replacement, cold and direct launches, shared bundle IDs, persistent path identity across standalone restarts, and selection after a running executable is deleted. The default identity check requires no Accessibility or Screen Recording grants. Add `--desktop` to read intact fixtures’ controls, verify AX button presses in their state files, capture their windows, and record observation results after executable deletion; this requires existing grants for both permissions. -Run `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py` in a GUI login session to verify that the helper owns its permission identity even when started through two nested GUI launchers. Concurrent connections report the same helper name and icon. The check owns its applications, cleans up LaunchServices registrations, and requests no Accessibility or Screen Recording access. +Run `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py` in a GUI login session to verify that the helper owns its permission identity even when started through two nested GUI launchers. Concurrent connections report the same helper name and icon. The check owns its applications, cleans up LaunchServices registrations, and requests no Accessibility or Screen Recording access. Run `python3 packages/experimental/cua-macos/native/tests/nested_application_e2e.py` to verify signature-preserving extraction outside an enclosing `.app`, concurrent publication, and service teardown with uniquely signed fixtures. Run `python3 packages/experimental/cua-macos/native/tests/axless_e2e.py` after building and unlocking the Mac to verify an owned window whose application removes and later restores its AX roots. The check requires existing Accessibility/Screen Recording grants, rejects stale element indices, captures the AXless window, verifies a coordinate click in the fixture state file, and checks restoration. It never changes system permission settings. diff --git a/packages/experimental/cua-macos/native/README.zh.md b/packages/experimental/cua-macos/native/README.zh.md index dea7f86ff6..a23958612f 100644 --- a/packages/experimental/cua-macos/native/README.zh.md +++ b/packages/experimental/cua-macos/native/README.zh.md @@ -23,7 +23,7 @@ kind: "package-reference" ## 构建与服务 -运行 `pnpm --dir packages/experimental/cua-macos run build:native`,在 `native/bin/darwin-` 下生成带签名的 `DSH Computer Use.app`,`current.json` 选择完整的内容摘要目录。默认使用临时签名;正式打包可传入 `--identity` 指定 Developer ID,并由发布流程完成公证。Host 运行应用内的 `--ensure-shared` 入口,经 LaunchServices 后台启动服务。最后一条连接完成清理后,服务退出。 +运行 `pnpm --dir packages/experimental/cua-macos run build:native`,在 `native/bin/darwin-` 下生成带签名的 `DSH Computer Use.app`,`current.json` 选择完整的内容摘要目录。默认使用临时签名;正式打包可传入 `--identity` 指定 Developer ID,并由发布流程完成公证。Host 运行应用内的 `--ensure-shared` 入口,经 LaunchServices 后台启动服务。最后一条连接完成清理后,服务退出。 helper 嵌套在另一个 `.app` 内时,启动器先把完整的已签名应用复制到 `~/Library/Application Support/DeepSeek Harness/Computer Use//`,再启动它。每个不可变目录都校验可执行文件哈希和应用签名,旧目录保留给仍在运行的安装版本使用,使两项 macOS 权限都归属于 helper。校验失败会中止启动;删除报错中指定的缓存目录后再重试。 源码检出以 `--variant dev` 构建,应用包为 `DSH Computer Use Dev.app`,Bundle ID 为 `com.deepseek.harness.computer-use.dev`。开发版和正式版默认临时签名。临时签名的指定要求包含代码哈希,二进制变化后可能需要重新授权。 @@ -37,7 +37,7 @@ kind: "package-reference" `native/.build/prepared/` 和 `native/bin/darwin-` 中的各代应用不会自动清理。正式打包时使用全新的 `--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)。打包无需 macOS 桌面登录或 Rosetta。在仓库根目录运行 `pnpm exec tsx scripts/cua-helper/package.ts --out ` 可执行相同准备。完整应用签名、公证和贴票后才确定最终内容摘要,再从解包后的 `cua-helper.tgz` 验证架构、签名、票据、Gatekeeper 和文件完整性。打包不启动 helper。在已登录的桌面会话中单独运行 `python3 scripts/cua-helper/smoke.py ` 检查能力、helper 身份和退出清理;Apple Silicon 上执行 x64 版本需要 Rosetta。该检查不申请权限或观察应用。命令失败、超时或启动回复无法解析时,会输出捕获的 stdout 和 stderr;无效编码只在诊断文本中替换,协议解析仍严格校验。打包成功不代表运行验收通过。 +手动工作流 **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)。打包无需 macOS 桌面登录或 Rosetta。在仓库根目录运行 `pnpm exec tsx scripts/cua-helper/package.ts --out ` 可执行相同准备。完整应用签名、公证和贴票后才确定最终内容摘要,再从解包后的 `cua-helper.tgz` 验证架构、签名、票据、Gatekeeper 和文件完整性。打包不启动 helper。在已登录的桌面会话中单独运行 `python3 scripts/cua-helper/smoke.py ` 检查能力、helper 身份和退出清理;Apple Silicon 上执行 x64 版本需要 Rosetta。该检查不申请权限或观察应用。命令失败、超时或启动回复无法解析时,会输出捕获的 stdout 和 stderr;无效编码只在诊断文本中替换,协议解析仍严格校验。打包成功不代表运行验收通过。 helper 打包保留独立的[诊断记录](../../../../apps/desktop/packaging/README.zh.md#macos-release-signing),包括桌面安装包开始构建前发生的失败。 工作流保留 `cua-helper.tgz` 和 `cua-helper.json`,后者记录产品版本、源码 commit、Team ID 和两份应用摘要。[Release publish](../../../../.github/workflows/release-publish.yml)在生成 provider 的 npm 包前接入该产物;普通 PR 发布检查不使用签名凭据。准备步骤拒绝源码或版本不匹配、架构不全、旧应用目录、链接、字节变化和执行权限缺失。npm 发布在写入注册表前重新检查实际 provider tarball。重新签名会改变产物字节,重试部分发布的版本时需保留原产物。 @@ -135,7 +135,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后 在已解锁的 macOS GUI 会话中运行 `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper`,需要可用的 Apple Swift 编译器。它创建并清理自己的 AppKit 应用和独立 GUI 进程,检查元数据替换、冷启动与直接启动、共用 Bundle ID、独立程序重启后的持久路径身份,以及删除运行中可执行文件后的选择行为。默认身份检查不需要辅助功能或屏幕录制权限。添加 `--desktop` 可读取完整测试应用的控件,通过状态文件验证 AX 按钮点击,截取窗口,并记录删除可执行文件后的观察结果;此模式要求已有两项系统权限。 -在 GUI 登录会话中运行 `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py`,验证两个嵌套 GUI 启动器启动 helper 后,授权归属仍为 helper 自己,多条连接得到相同名称和图标。检查自行创建应用并清理 LaunchServices 注册,不申请辅助功能或屏幕录制权限。 +在 GUI 登录会话中运行 `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py`,验证两个嵌套 GUI 启动器启动 helper 后,授权归属仍为 helper 自己,多条连接得到相同名称和图标。检查自行创建应用并清理 LaunchServices 注册,不申请辅助功能或屏幕录制权限。 运行 `python3 packages/experimental/cua-macos/native/tests/nested_application_e2e.py`,用独立签名的测试应用验证 helper 在外层 `.app` 之外保留签名发布、并发发布和服务退出。 构建并解锁 Mac 后运行 `python3 packages/experimental/cua-macos/native/tests/axless_e2e.py`,验证应用移除并恢复 AX 根节点的窗口。检查需要已有辅助功能/屏幕录制权限,会拒绝过期元素索引,捕获无 AX 窗口,通过 fixture 状态文件核实坐标点击,并检查恢复。不会修改系统权限设置。 diff --git a/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift index 42c1853870..c468d95032 100644 --- a/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift +++ b/packages/experimental/cua-macos/native/Sources/CuaNative/SharedHelperRuntime.swift @@ -296,6 +296,7 @@ final class SharedSocket: @unchecked Sendable { throw NativeFailure(code: "NATIVE_APPLICATION_REQUIRED", message: "Computer Use requires its signed application bundle; set helperPath to DSH Computer Use.app/Contents/MacOS/dsh-cua-native") } + let applicationURL = try StandaloneApplication.prepare(bundle.bundleURL, runtimeKey: options.runtimeKey, deadline: deadline) let configuration = NSWorkspace.OpenConfiguration() configuration.activates = false configuration.addsToRecentItems = false @@ -310,12 +311,12 @@ final class SharedSocket: @unchecked Sendable { guard DispatchTime.now().uptimeNanoseconds < deadline else { throw NativeFailure(code: "NATIVE_STARTUP_TIMEOUT", message: "Computer Use launch lock exhausted the startup deadline") } - // LaunchServices gives the application its own TCC identity; direct spawning inherits the terminal's. + // LaunchServices and a standalone bundle keep both permission services attributed to this helper. let application: NSRunningApplication - do { application = try await NSWorkspace.shared.openApplication(at: bundle.bundleURL, configuration: configuration) } + do { application = try await NSWorkspace.shared.openApplication(at: applicationURL, configuration: configuration) } catch { if let failure = try SharedHelperOptions.startupFailure(at: failurePath) { throw failure } - throw NativeFailure(code: "NATIVE_STARTUP", message: "macOS could not launch Computer Use at \(bundle.bundleURL.path): \(error.localizedDescription)") + throw NativeFailure(code: "NATIVE_STARTUP", message: "macOS could not launch Computer Use at \(applicationURL.path): \(error.localizedDescription)") } let pid = application.processIdentifier guard pid > 0 else { diff --git a/packages/experimental/cua-macos/native/Sources/CuaNative/StandaloneApplication.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/StandaloneApplication.swift new file mode 100644 index 0000000000..15bd509639 --- /dev/null +++ b/packages/experimental/cua-macos/native/Sources/CuaNative/StandaloneApplication.swift @@ -0,0 +1,86 @@ +// Nested applications share their enclosing app's Screen Recording identity in TCC. +import CryptoKit +import Darwin +import Foundation +import Security + +enum StandaloneApplication { + static func hasApplicationAncestor(_ application: URL) -> Bool { + var parent = application.deletingLastPathComponent() + while parent.path != "/" { + if parent.pathExtension.lowercased() == "app" { return true } + parent.deleteLastPathComponent() + } + return false + } + + static func prepare(_ application: URL, runtimeKey: String, deadline: UInt64) throws -> URL { + let source = application.resolvingSymlinksInPath() + guard hasApplicationAncestor(source) else { return source } + let support = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, + appropriateFor: nil, create: true) + return try install(source, runtimeKey: runtimeKey, + root: support.appendingPathComponent("DeepSeek Harness/Computer Use"), deadline: deadline) + } + + // Published generations remain immutable while any installed DSH version may still run them. + static func install(_ source: URL, runtimeKey: String, root: URL, deadline: UInt64) throws -> URL { + let files = FileManager.default + try files.createDirectory(at: root, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700]) + let root = root.resolvingSymlinksInPath() + let generation = root.appendingPathComponent(runtimeKey, isDirectory: true) + let application = generation.appendingPathComponent(source.lastPathComponent, isDirectory: true) + guard !hasApplicationAncestor(application) else { + throw NativeFailure(code: "NATIVE_APPLICATION_REQUIRED", message: "Computer Use application storage must be outside another .app: \(root.path)") + } + var lock: SharedHelperLock? + repeat { + lock = try SharedHelperLock(path: root.appendingPathComponent("\(runtimeKey).lock").path) + if lock != nil { break } + usleep(10_000) + } while DispatchTime.now().uptimeNanoseconds < deadline + guard let lock else { + throw NativeFailure(code: "NATIVE_STARTUP_TIMEOUT", message: "Computer Use application installation is still in progress") + } + defer { withExtendedLifetime(lock) {} } + if files.fileExists(atPath: generation.path) { + do { try validate(application, runtimeKey: runtimeKey) } + catch { + throw NativeFailure(code: "NATIVE_APPLICATION_INVALID", message: "Computer Use cached application is invalid: \(error.localizedDescription); remove \(generation.path) and retry") + } + return application + } + let staging = root.appendingPathComponent(".staging-\(UUID().uuidString)", isDirectory: true) + try files.createDirectory(at: staging, withIntermediateDirectories: false, attributes: [.posixPermissions: 0o700]) + do { + let stagedApplication = staging.appendingPathComponent(source.lastPathComponent, isDirectory: true) + try files.copyItem(at: source, to: stagedApplication) + try validate(stagedApplication, runtimeKey: runtimeKey) + try files.moveItem(at: staging, to: generation) + } catch { + let failure = error + do { try files.removeItem(at: staging) } + catch { + throw NativeFailure(code: "NATIVE_IO", message: "Computer Use installation failed: \(failure.localizedDescription); could not remove \(staging.path): \(error.localizedDescription)") + } + throw failure + } + return application + } + + private static func validate(_ application: URL, runtimeKey: String) throws { + guard !hasApplicationAncestor(application.resolvingSymlinksInPath()) else { + throw NativeFailure(code: "NATIVE_APPLICATION_REQUIRED", message: "Computer Use application resolves inside another .app: \(application.path)") + } + let executable = application.appendingPathComponent("Contents/MacOS/dsh-cua-native") + let bytes = try Data(contentsOf: executable, options: .mappedIfSafe) + let digest = SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined() + var code: SecStaticCode? + let flags = SecCSFlags(rawValue: kSecCSStrictValidate | kSecCSCheckAllArchitectures | kSecCSCheckNestedCode) + guard digest == runtimeKey, + SecStaticCodeCreateWithPath(application as CFURL, [], &code) == errSecSuccess, + let code, SecStaticCodeCheckValidity(code, flags, nil) == errSecSuccess else { + throw NativeFailure(code: "NATIVE_APPLICATION_INVALID", message: "Computer Use application failed signature or executable verification at \(application.path)") + } + } +} diff --git a/packages/experimental/cua-macos/native/tests/nested_application_e2e.py b/packages/experimental/cua-macos/native/tests/nested_application_e2e.py new file mode 100644 index 0000000000..b8b3b3d994 --- /dev/null +++ b/packages/experimental/cua-macos/native/tests/nested_application_e2e.py @@ -0,0 +1,79 @@ +"""Verify nested helper extraction and concurrent launchers without granting macOS permissions.""" +import argparse +import concurrent.futures +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import Path +import plistlib +import shutil +import signal +import subprocess +import tempfile +import uuid + +from native_test_support import copy_helper, default_binary, unregister_helper +from shared_helper_e2e import Connection, wait_released + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--helper', type=Path, default=None) + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix='cua-nest-', dir='/tmp') as temporary, ExitStack() as cleanup: + root = Path(temporary) + parent = root / 'Desktop.app' / 'Contents' / 'Resources' + parent.mkdir(parents=True) + helper = copy_helper(args.helper or default_binary(), parent) + application = helper.parents[2] + manifest = application / 'Contents/Info.plist' + info = plistlib.loads(manifest.read_bytes()) + info['CFBundleIdentifier'] = 'dev.dsh.cua.nested-test.' + uuid.uuid4().hex + manifest.write_bytes(plistlib.dumps(info)) + subprocess.run(['codesign', '--force', '--sign', '-', '--timestamp=none', str(application)], + check=True, capture_output=True, timeout=30) + key = hashlib.sha256(helper.read_bytes()).hexdigest() + cache = Path.home() / 'Library/Application Support/DeepSeek Harness/Computer Use' + generation = cache / key + assert not generation.exists(), 'The unique signed fixture unexpectedly shares an existing generation' + cleanup.callback(lambda: (cache / (key + '.lock')).unlink(missing_ok=True)) + cleanup.callback(lambda: shutil.rmtree(generation) if generation.exists() else None) + copied_helper = generation / application.name / 'Contents/MacOS/dsh-cua-native' + cleanup.callback(unregister_helper, copied_helper) + runtime = [root / 'a', root / 'b'] + + def stop_owned(directory, pid): + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + pass + wait_released(directory / (key + '.lock'), pid) + + def launch(directory): + result = subprocess.run([str(helper), '--ensure-shared', '--runtime-key', key, + '--runtime-directory', str(directory), '--startup-timeout-ms', '15000'], + check=True, capture_output=True, text=True, timeout=25) + ready = json.loads(result.stdout) + cleanup.callback(stop_owned, directory, ready['pid']) + client = Connection(directory / (key + '.sock'), key) + cleanup.callback(client.close) + return client + + # Separate launcher processes share the installation lock while retaining independent service sockets. + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as workers: + clients = list(workers.map(launch, runtime)) + for client in clients: + state = client.call('permissionStatus', {}) + assert Path(state['helper']['path']).resolve() == copied_helper.resolve(), state + assert Path(state['launchApplication']['path']).resolve() == copied_helper.parents[2].resolve(), state + assert not any(part.endswith('.app') for part in copied_helper.parents[3].parts), state + assert clients[0].ready['pid'] != clients[1].ready['pid'] + assert copied_helper.read_bytes() == helper.read_bytes() + subprocess.run(['codesign', '--verify', '--deep', '--strict', str(copied_helper.parents[2])], + check=True, capture_output=True, timeout=30) + print('PASS: concurrent launchers use the same signature-preserving standalone application and drain both helpers') + + +if __name__ == '__main__': + main() diff --git a/packages/experimental/cua-macos/native/tests/swift/StandaloneApplicationTests.swift b/packages/experimental/cua-macos/native/tests/swift/StandaloneApplicationTests.swift new file mode 100644 index 0000000000..a73dda8516 --- /dev/null +++ b/packages/experimental/cua-macos/native/tests/swift/StandaloneApplicationTests.swift @@ -0,0 +1,136 @@ +// Real signed fixtures exercise cache publication, reuse, integrity and isolated permission ownership. +import CryptoKit +import Darwin +import Foundation +import Testing +@testable import CuaNative + +private struct ApplicationFixture { + let root: URL + let application: URL + let cache: URL + var executable: URL { application.appendingPathComponent("Contents/MacOS/dsh-cua-native") } + var resource: URL { application.appendingPathComponent("Contents/Resources/value.txt") } + var key: String { get throws { SHA256.hash(data: try Data(contentsOf: executable)).map { String(format: "%02x", $0) }.joined() } } + var deadline: UInt64 { DispatchTime.now().uptimeNanoseconds + 30_000_000_000 } + + init() throws { + root = FileManager.default.temporaryDirectory.appendingPathComponent("cua-application-\(UUID().uuidString)") + application = root.appendingPathComponent("Desktop.app/Contents/Resources/DSH Computer Use.app") + cache = root.appendingPathComponent("support") + try FileManager.default.createDirectory(at: executable.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: resource.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.copyItem(at: URL(fileURLWithPath: "/usr/bin/true"), to: executable) + try Data("original resource".utf8).write(to: resource) + let plist = try PropertyListSerialization.data(fromPropertyList: [ + "CFBundleIdentifier": "dev.dsh.cua.cache-test.\(UUID().uuidString)", + "CFBundleExecutable": "dsh-cua-native", "CFBundleName": "DSH Computer Use", "CFBundlePackageType": "APPL", + ], format: .xml, options: 0) + try plist.write(to: application.appendingPathComponent("Contents/Info.plist")) + try sign() + } + + func sign() throws { + let process = Process(), finished = DispatchSemaphore(value: 0) + process.executableURL = URL(fileURLWithPath: "/usr/bin/codesign") + process.arguments = ["--force", "--sign", "-", "--timestamp=none", application.path] + process.standardOutput = FileHandle.nullDevice + process.standardError = FileHandle.nullDevice + process.terminationHandler = { _ in finished.signal() } + try process.run() + if finished.wait(timeout: .now() + 30) == .timedOut { + kill(process.processIdentifier, SIGKILL) + process.waitUntilExit() + throw NativeFailure(code: "FIXTURE_TIMEOUT", message: "Ad-hoc fixture signing did not exit") + } + #expect(process.terminationReason == .exit) + try #require(process.terminationStatus == 0) + } + + func close() { + do { try FileManager.default.removeItem(at: root) } + catch { Issue.record("Could not remove owned application fixture: \(error)") } + } +} + +@Test func onlyNestedApplicationsNeedAnIndependentInstallation() throws { + #expect(StandaloneApplication.hasApplicationAncestor(URL(fileURLWithPath: "/Applications/Desktop.app/Contents/Helper.app"))) + #expect(StandaloneApplication.hasApplicationAncestor(URL(fileURLWithPath: "/Applications/Desktop.APP/Helper.app"))) + let standalone = URL(fileURLWithPath: "/Users/example/Helper.app") + #expect(!StandaloneApplication.hasApplicationAncestor(standalone)) + #expect(try StandaloneApplication.prepare(standalone, runtimeKey: "unused", deadline: 0) == standalone) +} + +@Test func aSignedHelperIsPublishedOutsideItsEnclosingApplicationAndReused() throws { + let f = try ApplicationFixture(); defer { f.close() } + let key = try f.key + let installed = try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) + #expect(!StandaloneApplication.hasApplicationAncestor(installed)) + #expect(try Data(contentsOf: installed.appendingPathComponent("Contents/MacOS/dsh-cua-native")) == Data(contentsOf: f.executable)) + #expect(try Data(contentsOf: installed.appendingPathComponent("Contents/Resources/value.txt")) == Data(contentsOf: f.resource)) + #expect(try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) == installed) +} + +@Test func changingTheSignedApplicationRetainsTheEarlierGeneration() throws { + let f = try ApplicationFixture(); defer { f.close() } + let old = try StandaloneApplication.install(f.application, runtimeKey: f.key, root: f.cache, deadline: f.deadline) + try Data("new resource".utf8).write(to: f.resource) + try f.sign() + let new = try StandaloneApplication.install(f.application, runtimeKey: f.key, root: f.cache, deadline: f.deadline) + #expect(old != new) + #expect(try String(contentsOf: old.appendingPathComponent("Contents/Resources/value.txt"), encoding: .utf8) == "original resource") + #expect(try String(contentsOf: new.appendingPathComponent("Contents/Resources/value.txt"), encoding: .utf8) == "new resource") +} + +@Test(arguments: ["Contents/Resources/value.txt", "Contents/MacOS/dsh-cua-native"]) +func aChangedCachedApplicationIsRejected(file: String) throws { + let f = try ApplicationFixture(); defer { f.close() } + let key = try f.key + let installed = try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) + try Data("changed".utf8).write(to: installed.appendingPathComponent(file)) + #expect(throws: (any Error).self) { + try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) + } +} + +@Test func anInvalidSourceLeavesNoPartialPublishedApplication() throws { + let f = try ApplicationFixture(); defer { f.close() } + let key = try f.key + try Data("unsigned change".utf8).write(to: f.resource) + #expect(throws: (any Error).self) { + try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) + } + #expect(try FileManager.default.contentsOfDirectory(atPath: f.cache.path) == ["\(key).lock"]) +} + +@Test func applicationStorageCannotItselfBeNestedInAnotherApplication() throws { + let f = try ApplicationFixture(); defer { f.close() } + #expect(throws: (any Error).self) { + try StandaloneApplication.install(f.application, runtimeKey: f.key, + root: f.root.appendingPathComponent("Other.app/cache"), deadline: f.deadline) + } +} + +@Test func aCachedSymlinkCannotRedirectTheHelperIntoAnEnclosingApplication() throws { + let f = try ApplicationFixture(); defer { f.close() } + let key = try f.key + try FileManager.default.createDirectory(at: f.cache, withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(at: f.cache.appendingPathComponent(key), + withDestinationURL: f.application.deletingLastPathComponent()) + #expect(throws: (any Error).self) { + try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, deadline: f.deadline) + } +} + +@Test func aCompetingInstallerDoesNotUseAPartialGeneration() throws { + let f = try ApplicationFixture(); defer { f.close() } + let key = try f.key + try FileManager.default.createDirectory(at: f.cache, withIntermediateDirectories: true) + let lock = try #require(try SharedHelperLock(path: f.cache.appendingPathComponent("\(key).lock").path)) + defer { withExtendedLifetime(lock) {} } + #expect(throws: (any Error).self) { + try StandaloneApplication.install(f.application, runtimeKey: key, root: f.cache, + deadline: DispatchTime.now().uptimeNanoseconds) + } + #expect(!FileManager.default.fileExists(atPath: f.cache.appendingPathComponent(key).path)) +} diff --git a/packages/experimental/cua-macos/src/native-build.ts b/packages/experimental/cua-macos/src/native-build.ts index 8c288bbdc3..7fb98289e9 100644 --- a/packages/experimental/cua-macos/src/native-build.ts +++ b/packages/experimental/cua-macos/src/native-build.ts @@ -117,12 +117,21 @@ export async function buildNativeExecutable(spec: NativeBuildSpec): Promise { if (await isSourcePackage(spec.packageRoot)) return buildNativeExecutable(spec) - const executable = await nativeExecutablePath(join(spec.packageRoot, 'native', 'bin', `darwin-${spec.architecture}`), 'release') + const directory = join(spec.packageRoot.replace(/([\\/])app\.asar(?=[\\/])/u, '$1app.asar.unpacked'), 'native', 'bin', `darwin-${spec.architecture}`) + let executable: string + try { executable = await nativeExecutablePath(directory, 'release') } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || (error as NodeJS.ErrnoException).path !== join(directory, 'current.json')) throw error + throw new Error(`Computer use helper application is missing from ${directory}; this installation does not include the signed helper`, { cause: error }) + } spec.signal.throwIfAborted() return executable } diff --git a/packages/experimental/cua-macos/tests/native-build.spec.ts b/packages/experimental/cua-macos/tests/native-build.spec.ts index 2d657374d6..599f69092d 100644 --- a/packages/experimental/cua-macos/tests/native-build.spec.ts +++ b/packages/experimental/cua-macos/tests/native-build.spec.ts @@ -113,6 +113,25 @@ it.each(['ad-hoc', { identity: 'Apple Development: Example', keychain: '/keychai await expect(prepareNativeExecutable(f.spec)).rejects.toThrow() }) +it('reads an application that an Electron archive installation keeps beside app.asar', async () => { + const f = await fixture() + const archived = join(f.root, 'Resources/app.asar/dsh/node_modules/@deepseek-ai/dsh-experimental-cua-macos') + const path = await publish(join(f.root, 'Resources/app.asar.unpacked/dsh/node_modules/@deepseek-ai/dsh-experimental-cua-macos/native/bin/darwin-arm64'), f.output, 'release') + expect(await prepareNativeExecutable({ ...f.spec, packageRoot: archived })).toBe(path) + expect(f.spawn).not.toHaveBeenCalled() +}) + +it('names the expected directory when an installed package carries no helper application', async () => { + const f = await fixture() + const installed = join(f.root, 'node_modules/@deepseek-ai/dsh-experimental-cua-macos.asar/package') + const directory = join(installed, 'native/bin/darwin-arm64') + await expect(prepareNativeExecutable({ ...f.spec, packageRoot: installed })).rejects.toThrow( + `Computer use helper application is missing from ${directory}; this installation does not include the signed helper`) + await mkdir(directory, { recursive: true }) + await writeFile(join(directory, 'current.json'), '{') + await expect(prepareNativeExecutable({ ...f.spec, packageRoot: installed })).rejects.toThrow(SyntaxError) +}) + it.each(['../outside', '/absolute', '', 'a'.repeat(63), 'A'.repeat(64)])('rejects an invalid installed generation %s', async (bundle) => { const f = await fixture() await mkdir(f.directory, { recursive: true }) diff --git a/packages/util/official-bundles/README.i18n.yaml b/packages/util/official-bundles/README.i18n.yaml index e843d9aadd..b58cb9c8e7 100644 --- a/packages/util/official-bundles/README.i18n.yaml +++ b/packages/util/official-bundles/README.i18n.yaml @@ -27,8 +27,8 @@ en: 27c22abdfff69a41 zh: e60d828cdb75c263 /deepseek-ai-dsh-official-bundles/use-this-package/on-demand-bundles: - en: ba4f412474854ba1 - zh: 17dae8155c013367 + en: 6ba39ba5bfefec79 + zh: 788a71b5b6f75404 /deepseek-ai-dsh-official-bundles/understand-the-implementation: en: 3fb2b58db529d8be zh: 46064459a23d458d diff --git a/packages/util/official-bundles/README.md b/packages/util/official-bundles/README.md index f1e0d1224b..9af85af79f 100644 --- a/packages/util/official-bundles/README.md +++ b/packages/util/official-bundles/README.md @@ -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. The catalog also preserves `listedByDefault` so a default-hidden bundle stays hidden until explicitly installed or enabled. +`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. ----- diff --git a/packages/util/official-bundles/README.zh.md b/packages/util/official-bundles/README.zh.md index 2ce46257d2..91c0722cf7 100644 --- a/packages/util/official-bundles/README.zh.md +++ b/packages/util/official-bundles/README.zh.md @@ -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 组合随附选择和按需选择,不执行提供方。 目录也保留 `listedByDefault`,使默认隐藏的组合包在用户显式安装或启用前保持隐藏。 +`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 组合随附选择和按需选择,不执行提供方。 ----- diff --git a/packages/util/official-bundles/src/bundles.ts b/packages/util/official-bundles/src/bundles.ts index b9e2d5a672..b84847a4aa 100644 --- a/packages/util/official-bundles/src/bundles.ts +++ b/packages/util/official-bundles/src/bundles.ts @@ -55,6 +55,7 @@ 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', @@ -74,5 +75,4 @@ 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', ] diff --git a/packages/util/official-bundles/src/catalog.generated.ts b/packages/util/official-bundles/src/catalog.generated.ts index 3a494f90e6..8a0cfb7579 100644 --- a/packages/util/official-bundles/src/catalog.generated.ts +++ b/packages/util/official-bundles/src/catalog.generated.ts @@ -20,14 +20,4 @@ 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=', - }, - }, ] diff --git a/packages/util/official-bundles/src/index.ts b/packages/util/official-bundles/src/index.ts index e84033d6f0..433ff321a4 100644 --- a/packages/util/official-bundles/src/index.ts +++ b/packages/util/official-bundles/src/index.ts @@ -17,8 +17,6 @@ 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> } diff --git a/packages/util/official-bundles/tests/official-bundles.spec.ts b/packages/util/official-bundles/tests/official-bundles.spec.ts index bffbafcaed..716355ffe6 100644 --- a/packages/util/official-bundles/tests/official-bundles.spec.ts +++ b/packages/util/official-bundles/tests/official-bundles.spec.ts @@ -4,10 +4,8 @@ import { describe, expect, it } from 'vitest' import { OFFICIAL_ON_DEMAND_CATALOG, ON_DEMAND_BUNDLES, PROFILE_TEMPLATES } from '../src/index.ts' describe('Official on-demand catalog', () => { - it('offers native subagents and default-hidden Computer Use with offline metadata', () => { - expect(ON_DEMAND_BUNDLES).toEqual(['@deepseek-ai/dsh-subagent-claude-code', '@deepseek-ai/dsh-subagent-codex', '@deepseek-ai/dsh-cua-bundle']) - expect(OFFICIAL_ON_DEMAND_CATALOG.find(entry => entry.packageName === '@deepseek-ai/dsh-cua-bundle')) - .toMatchObject({ platforms: ['darwin'], listedByDefault: false }) + it('offers exactly the two native subagent integrations with offline localized metadata', () => { + expect(ON_DEMAND_BUNDLES).toEqual(['@deepseek-ai/dsh-subagent-claude-code', '@deepseek-ai/dsh-subagent-codex']) expect(OFFICIAL_ON_DEMAND_CATALOG.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES) for (const entry of OFFICIAL_ON_DEMAND_CATALOG) { expect(entry.meta.icon).toMatch(/^data:image\/svg\+xml;base64,/) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0235904d11..0fb7c4b2d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,6 +336,9 @@ 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-auto-review': specifier: workspace:* version: link:../../packages/experimental/auto-review diff --git a/scripts/cua-helper/package.spec.ts b/scripts/cua-helper/package.spec.ts new file mode 100644 index 0000000000..a4c5b85202 --- /dev/null +++ b/scripts/cua-helper/package.spec.ts @@ -0,0 +1,114 @@ +/** Helper packaging retains redacted Apple failures independently of desktop installer startup. */ +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { expect, it, vi } from 'vitest' + +const state = vi.hoisted(() => ({ root: '', directory: '', requestedRoot: '', reject: false })) +vi.mock('../../apps/desktop/scripts/desktop-package-environment.mjs', () => ({ + loadDesktopPackageEnvironment: () => ({ APPLE_ID: 'private@example.test', APPLE_PASSWORD: 'secret-sentinel' }), +})) +vi.mock('../../apps/desktop/scripts/desktop-release-environment.mjs', () => ({ + resolveMacOSSigningEnvironment: () => ({ signingIdentity: 'Fixture (ABCDEFGHIJ)', teamId: 'ABCDEFGHIJ' }), + resolveMacOSNotarizationEnvironment: () => ({ appleId: 'private@example.test', appleIdPassword: 'secret-sentinel', appleTeamId: 'ABCDEFGHIJ' }), +})) +vi.mock('../../apps/desktop/scripts/macos-package-settings.mjs', () => ({ resolveMacOSPackageSettings: () => ({}) })) +vi.mock('../../apps/desktop/scripts/macos-signing-keychain.mjs', () => ({ + withMacOSSigningKeychain: async (environment: object, action: (environment: object) => Promise) => action(environment), +})) +vi.mock('../../apps/desktop/scripts/macos-notarization-proxy.ts', () => ({ + withMacOSNotarizationProxy: async (_settings: object, action: () => Promise) => action(), +})) +vi.mock('../../apps/desktop/scripts/verify-macos-signature.mjs', () => ({ verifyMacOSRuntimeCode: () => {} })) +vi.mock('../../apps/desktop/scripts/packaging-run.mjs', async (importOriginal) => { + const original = await importOriginal() + return { ...original, createPackagingRun: (...args: Parameters) => { + state.requestedRoot = args[0] + const run = original.createPackagingRun(join(state.root, 'records'), args[1], args[2]) + state.directory = run.directory + return run + } } +}) +vi.mock('../../apps/desktop/scripts/notarize-macos.mjs', () => ({ + notarizeMacOS: async () => { + const { runLoggedNotarytool } = await import('../../apps/desktop/scripts/logged-notarytool.mjs') + const id = '11111111-2222-3333-4444-555555555555' + await runLoggedNotarytool(['submit', '/fixture/helper.zip', '--apple-id', 'private@example.test', + '--password', 'secret-sentinel', '--wait', '--output-format', 'json'], process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, async (args) => { + if (state.reject) throw new Error('Apple transport failed: private@example.test secret-sentinel') + return JSON.stringify(args[0] === 'submit' ? { id } : { id, status: 'Accepted' }) + }) + }, +})) +vi.mock('execa', () => ({ execa: async (_command: string, args: string[]) => { + const output = args.indexOf('--output-directory') + if (output !== -1) await mkdir(args[output + 1]!, { recursive: true }) +} })) +vi.mock('tar', () => ({ + create: async ({ file }: { file: string }) => writeFile(file, 'fixture archive'), + extract: async () => {}, +})) +vi.mock('./payload.ts', async importOriginal => ({ + ...await importOriginal(), + readCuaApplication: (directory: string) => ({ application: join(directory, 'fixture/DSH Computer Use.app') }), + sealCuaApplication: () => 'fixture', + verifyCuaHelperPayload: () => {}, +})) + +it.each([false, true])('retains helper packaging outcome and restores the inherited journal when Apple rejects=%s', async (reject) => { + state.root = await mkdtemp(join(tmpdir(), 'cua-package-log-')) + state.directory = '' + state.reject = reject + const output = join(state.root, 'output') + const savedPlatform = Object.getOwnPropertyDescriptor(process, 'platform')! + const savedArgv = process.argv + const savedDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) + const inherited = reject ? join(state.root, 'outer-record') : undefined + const listeners = ['SIGINT', 'SIGTERM'].map(signal => process.listenerCount(signal)) + try { + Object.defineProperty(process, 'platform', { ...savedPlatform, value: 'darwin' }) + process.argv = [process.execPath, resolve('scripts/cua-helper/package.ts'), '--out', output] + if (inherited === undefined) delete process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = inherited + vi.resetModules() + const work = import('./package.ts') + if (reject) await expect(work).rejects.toThrow('CUA helper packaging failed; see') + else { + try { await work } catch (error) { + throw new Error(stderr.mock.calls.map(([chunk]) => String(chunk)).join(''), { cause: error }) + } + } + expect(state.requestedRoot).toBe(resolve('apps/desktop/.desktop-build/packaging-runs')) + expect(state.directory).not.toBe('') + const events = await readFile(join(state.directory, 'events.jsonl'), 'utf8') + const result = await readFile(join(state.directory, 'result.json'), 'utf8') + const terminal = stderr.mock.calls.map(([chunk]) => String(chunk)).join('') + expect(JSON.parse(result)).toMatchObject({ success: !reject }) + expect(events).toContain('notarytool:upload:helper.zip') + for (const diagnostic of [events, result, terminal]) { + expect(diagnostic).not.toContain('private@example.test') + expect(diagnostic).not.toContain('secret-sentinel') + } + if (reject) { + expect(events).toContain('Apple transport failed: [REDACTED] [REDACTED]') + expect(terminal).toContain('Apple transport failed: [REDACTED] [REDACTED]') + expect(events).not.toContain('notarytool:wait') + await expect(stat(join(output, 'cua-helper.tgz'))).rejects.toMatchObject({ code: 'ENOENT' }) + } else { + expect(events).toContain('"status":"Accepted"') + expect(await readFile(join(output, 'cua-helper.tgz'), 'utf8')).toBe('fixture archive') + } + expect(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR).toBe(inherited) + expect(['SIGINT', 'SIGTERM'].map(signal => process.listenerCount(signal))).toEqual(listeners) + } finally { + Object.defineProperty(process, 'platform', savedPlatform) + process.argv = savedArgv + if (savedDirectory === undefined) delete process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = savedDirectory + stderr.mockRestore() + consoleLog.mockRestore() + await rm(state.root, { recursive: true, force: true }) + } +}) diff --git a/scripts/cua-helper/package.ts b/scripts/cua-helper/package.ts index 408503691c..c2f6170178 100644 --- a/scripts/cua-helper/package.ts +++ b/scripts/cua-helper/package.ts @@ -2,7 +2,7 @@ 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 { basename, join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { execa } from 'execa' import { create, extract } from 'tar' @@ -12,6 +12,8 @@ import { withMacOSSigningKeychain } from '../../apps/desktop/scripts/macos-signi 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 { createPackagingRun } from '../../apps/desktop/scripts/packaging-run.mjs' +import { packagingErrorDetails, packagingStep } from '../../apps/desktop/scripts/packaging-step.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' @@ -27,13 +29,22 @@ async function main(): Promise { 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 secrets = Object.entries(environment).filter(([name]) => /KEY|SECRET|TOKEN|PASSWORD|APPLE_ID/iu.test(name)).map(([, value]) => value ?? '') + const journal = createPackagingRun(join(root, 'apps/desktop/.desktop-build/packaging-runs'), { + target: 'cua-helper', version, commit, + }) + console.log(`CUA_PACKAGING_RECORD ${journal.directory}`) const scratch = mkdtempSync(join(tmpdir(), 'dsh-cua-package-')) + const previousDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = journal.directory 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 => { - await execa(command, args, { cwd: root, env, extendEnv: false, stdio: 'inherit', cancelSignal: controller.signal, killDescendants: true }) + await packagingStep(journal.directory, basename(command), async () => { + await execa(command, args, { cwd: root, env, extendEnv: false, stdio: 'inherit', cancelSignal: controller.signal, killDescendants: true }) + }, secrets) } const verify = async (application: string, architecture: 'arm64' | 'x64'): Promise => { verifyMacOSRuntimeCode(application, signing) @@ -73,13 +84,18 @@ async function main(): Promise { writeFileSync(join(output, CUA_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`) console.log(`CUA helper artifact: ${archive}`) } catch (error) { + process.stderr.write(`${packagingErrorDetails(error, secrets)}\n`) + journal.finish(false) rmSync(join(output, 'cua-helper.tgz'), { force: true }) - throw error + throw new Error(`CUA helper packaging failed; see ${journal.directory}/events.jsonl`) } finally { process.off('SIGINT', stop) process.off('SIGTERM', stop) + if (previousDirectory === undefined) delete process.env.DSH_DESKTOP_PACKAGING_RUN_DIR + else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = previousDirectory rmSync(scratch, { recursive: true, force: true }) } + journal.finish(true) } await main() diff --git a/scripts/gen-official-bundle-catalog.spec.ts b/scripts/gen-official-bundle-catalog.spec.ts index 8ca4381dce..316f4ea800 100644 --- a/scripts/gen-official-bundle-catalog.spec.ts +++ b/scripts/gen-official-bundle-catalog.spec.ts @@ -81,17 +81,6 @@ 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 } } }) diff --git a/scripts/gen-official-bundle-catalog.ts b/scripts/gen-official-bundle-catalog.ts index b9f703b927..4336e998c2 100644 --- a/scripts/gen-official-bundle-catalog.ts +++ b/scripts/gen-official-bundle-catalog.ts @@ -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() + const manifests = new Map() 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,15 +30,12 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri dependencies?: Record optionalDependencies?: Record peerDependencies?: Record - dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms']; listedByDefault?: boolean } } + dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms'] } } } 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() @@ -80,9 +77,7 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri } } } - return { packageName, - ...entry.listedByDefault === undefined ? {} : { listedByDefault: entry.listedByDefault }, - ...entry.platforms === undefined ? {} : { platforms: entry.platforms }, + return { packageName, ...entry.platforms === undefined ? {} : { platforms: entry.platforms }, meta: { title: meta.title, description: meta.description, icon: meta.icon } } }) } @@ -100,7 +95,6 @@ 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` diff --git a/scripts/local-bundle-registry.spec.ts b/scripts/local-bundle-registry.spec.ts index ea047d8aa6..fb2f9fc1e5 100644 --- a/scripts/local-bundle-registry.spec.ts +++ b/scripts/local-bundle-registry.spec.ts @@ -133,7 +133,8 @@ describe('catalog and local closure', () => { // A vendor package keeps its own version line, so a run must not assume one shared version. expect(packages.get('@deepseek-ai/cordis')?.manifest.version) .not.toBe(packages.get('@deepseek-ai/dsh-brand')?.manifest.version) - expect(selection.foreignTargets).toEqual(process.platform === 'darwin' ? [] : ['@deepseek-ai/dsh-experimental-cua-macos']) + // Nothing in this closure is built for another platform, on any host. + expect(selection.foreignTargets).toEqual([]) }) it('follows install, optional, and required peer edges but not optional peers', () => { diff --git a/scripts/macos-package-workflow.spec.ts b/scripts/macos-package-workflow.spec.ts index a0bb2cb027..cb2f07cd39 100644 --- a/scripts/macos-package-workflow.spec.ts +++ b/scripts/macos-package-workflow.spec.ts @@ -71,6 +71,19 @@ describe.each(['arm64', 'x64'])('macOS %s packaging', (arch) => { expect(workflow.jobs.package!.env.PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL).toBe('${{ inputs.compression_level }}') }) + it('stages the notarized Computer Use helpers after the configuration check and before packing the runtime', () => { + const steps = workflow.jobs.package!.steps + const staging = steps.findIndex(step => step.name === 'Build and stage notarized Computer Use helpers') + expect(staging).toBeGreaterThan(steps.findIndex(step => step.name === 'Check packaging configuration and toolchain')) + expect(staging).toBeLessThan(steps.findIndex(step => step.name === 'Build signed installers')) + const run = steps[staging]?.run ?? '' + // Sources whose dsh does not ship the bundle, or that predate helper packaging, skip staging. + expect(run.indexOf("dependencies?.['@deepseek-ai/dsh-cua-bundle']")).toBeLessThan(run.indexOf('scripts/cua-helper/package.ts --out')) + expect(run.indexOf('[[ ! -f scripts/cua-helper/package.ts ]]')).toBeLessThan(run.indexOf('scripts/cua-helper/package.ts --out')) + expect(run).toContain('pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper') + expect(run).toContain('pnpm exec tsx scripts/cua-helper/stage.ts --from dist/cua-helper/extracted') + }) + it('passes the same target to preparation, packaging and retention', () => { const job = workflow.jobs.package! expect(job.env.DSH_DESKTOP_TARGET_PLATFORM).toBe('darwin') diff --git a/snapshots/web/creation-recommendations/session.v4.jsonl b/snapshots/web/creation-recommendations/session.v4.jsonl index 5465653a09..4d796fef77 100644 --- a/snapshots/web/creation-recommendations/session.v4.jsonl +++ b/snapshots/web/creation-recommendations/session.v4.jsonl @@ -16,7 +16,7 @@ {"type":"session/title","data":{"title":"I wish I had a","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_creation_skill","name":"skill","arguments":"{\"name\":\"personalized-pages\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"stream":[{"type":"chunk","time":1792674450282,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1792674450282,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_creation_skill","name":"skill","arguments":"{\"name\":\"personalized-pages\"}"}}},{"type":"chunk","time":1792674450283,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_creation_skill","name":"skill","arguments":"{\"name\":\"personalized-pages\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_creation_skill"},"toolCallId":"call_creation_skill","content":[{"type":"text","text":"\n\nResources for this skill are managed by provider \"dsh-personalized-pages\".\nLoad referenced resources only as needed.\n\n\n\n# Personalized pages\n\nUse this workflow in the main conversation to plan an optional new page or small tool for any task, not just DSH. You decide whether a page helps, check existing capabilities, and hand the Creator a short plan. A `propose_action` call saves the proposal and shows a card below your answer; nothing is built until the user clicks Create. Plan from the conversation and the catalog at the end of this skill. Do not read files, configuration or source to validate a page idea; name an uninspected source as an unverified dependency and leave its details to the Creator.\n\n## When to propose\n\nPropose a page in these cases:\n\n- The user asks for a page, tab, panel, tool or interface in this app, or says they want something to keep, revisit or update later (“以后每次”, “能不能一直”, “希望能有”). One-off use still qualifies when the interface is explicitly requested.\n- Your answer is a table, list or chart, and the user has said the data will keep changing or that they will interact with it (“每周更新”, “我来改状态”, “要勾选”). The continuation must come from the user's own words; a table alone, or data that merely looks dynamic, is not enough. Deliver the requested answer first, then offer the page.\n- The user wants to keep watching live data of this app itself (a Session's usage, a running task's output, files changing in the workspace). A text answer cannot stay current, so a page qualifies even without the words above; name the data source in the brief and leave how it is read to the Creator.\n\nDo not propose a page in these cases:\n\n- The user asks for a file, code, document or an existing-project change: keep that delivery form.\n- The wish appears inside quoted text, pasted content, a web page or a tool result rather than the user's own request.\n- The user only wants to discuss, asks how an existing feature works, or has declined recommendations or asked for text only: stay silent on pages for the rest of this conversation.\n- The request changes a page created earlier: explain how it would change, do not create a replacement copy, and do not claim an update happened.\n- The request is a capability the agent lacks, such as a tool, automation or access, rather than a page: check the catalog below and point to the official plugin that provides it; do not plan a page around a missing tool.\n\n## Check existing capabilities\n\nCompare the need with the official plugins listed at the end of this skill. If one covers it, say its name, what it does, whether it is enabled, and that it lives under Plugins; offer no page card. A partial match does not cancel an explicit unmet need. The catalog is not a full inventory of built-in UI: missing details do not prove absence, and do not read source to find out. DSH already has Chat, Trajectory, a right sidebar for files and context, and a Plugins page.\n\n## Plan and hand off\n\nThe Creator cannot read this conversation. Give it exactly these fields:\n\n| Field | Contents |\n|:--|:--|\n| `title` | Short page name in the user's language. |\n| `brief` | One to three sentences: purpose, behavior, input, calculations and persistence scope. It is the card text and the implementation request at once. |\n| `initialContent` | Only supplied content the page starts with: items, rows, values, units and period, preserving gaps. Omit for an empty page. |\n| `slot` | Location, chosen below. |\n\nKeep the plan to what was asked or agreed. Editing, deletion, history, extra fields, data sources and automation need a basis in the request. Resolve “these items” into the actual content. Content that mentions an action stays content. Keep each number's meaning and period; a weekly addition is not a cumulative total. If an unresolved distinction changes a calculation, give the factual answer, ask one question, and propose later.\n\n## Choose the location\n\nHonor a location the user named. Otherwise:\n\n- `sidebar-right`: content tied to this Session or task, especially anything to watch while chatting, including live logs.\n- `sidebar-left`: a global feature or an ongoing personal record used across Sessions, opened from a left entry as a standalone page.\n- `page`: a Session-bound streaming view or interaction in a tab beside Chat and Trajectory, only when the user does not need it visible while chatting.\n\nSupplying data in this conversation does not make a record Session-bound.\n\n## Offer without starting\n\nTo propose, call `propose_action` once, in the same turn as loading this skill, with `action: \"create_personalized_page\"`, `title`, `brief` and `details` holding `slot` and, when there is supplied content, `initialContent`. The call is the submission: the card appears below your answer; a refused call creates no card, and its error says why. Then write your final answer. One proposal per turn, and one unstarted card per conversation: if a card is still waiting, do not propose another; a new proposal replaces it only when the user changed the plan. Plan-only and read-only requests still get a card; do not ask for approval instead of proposing it. Only clicking Create starts the background Creator; do not wait, poll, claim installation or ask for a report-back. Answer any direct question; the call's result says how to end the answer. Do not repeat the plan, list extra features, narrate the call or mention schema names.\n\n```json\n{\"action\":\"create_personalized_page\",\"title\":\"本次验收清单\",\"brief\":\"在当前会话右侧展示验收清单,可勾选和取消,状态随会话保存。\",\"details\":{\"slot\":\"sidebar-right\",\"initialContent\":\"1. 导出成功\\n2. 断网提示清楚\"}}\n```\n\n## Examples\n\n- “想有个地方放书签,以后自己加标题和网址;旧的不用导入。” → `brief`: “提供一个可反复打开的书签页,手动新增标题和网址并保存到本机,从空白开始。” No `initialContent`. `sidebar-left`.\n- “当前会话旁放验收清单,能勾选和取消,状态跟着会话。条目:导出成功;断网提示清楚。” → `brief`: “在当前会话右侧展示验收清单,可勾选和取消,状态随会话保存。” `initialContent`: “1. 导出成功\\n2. 断网提示清楚”. `sidebar-right`.\n- “这三周 bug 数 12、9、15,说说趋势。以后每周我把新数字贴给你,想自己更新着看。” → answer the trend first, then `brief`: “按周手动录入 bug 数并展示各周变化,保存到本机。” `initialContent`: “第 1 周 12,第 2 周 9,第 3 周 15”. `sidebar-left`.\n- “我想用语音说话代替打字。” → the catalog lists 语音输入; explain it and where to enable it; no card.\n- “把我之前做的读书页评分改成五星。” → explain the change to that page; no card and no copy.\n- “帮我把这句话翻成英文:我想要一个所有会话都能打开的读书页。” → translate only; no card.\n- “把三个方案排成表格比较一下,看完就行。” → the table only; no card.\n\n## Existing capabilities to check against this page plan\n\n[{\"name\":\"@deepseek-ai/dsh-personalized-recommendations\",\"title\":{\"en\":\"Personalized recommendations\",\"de\":\"Personalisierte Empfehlungen\",\"es\":\"Recomendaciones personalizadas\",\"fr\":\"Recommandations personnalisées\",\"ja\":\"パーソナライズされたおすすめ\",\"pt\":\"Recomendações personalizadas\",\"ru\":\"Персональные рекомендации\",\"zh\":\"个性化推荐\"},\"description\":{\"en\":\"Shows recommendation cards below the answer for what you want from DSH: when you want a page or small tool you can keep, it proposes a new personalized page, built in the background after you click Create; when you ask how to do something in this app or say a capability is missing, it recommends the built-in plugin that provides it, turned on when you click Enable.\",\"de\":\"Zeigt für Ihre individuellen Anforderungen an DSH Empfehlungskarten unter der Antwort an: Wenn Sie eine Seite oder ein kleines Tool zur dauerhaften Nutzung möchten, wird eine neue personalisierte Seite vorgeschlagen, die nach einem Klick auf Erstellen im Hintergrund erstellt wird; wenn Sie fragen, wie etwas in dieser App geht, oder sagen, dass eine Funktion fehlt, wird das integrierte Plugin empfohlen, das sie bereitstellt, und nach einem Klick auf Aktivieren eingeschaltet.\",\"es\":\"Muestra tarjetas de recomendación debajo de la respuesta para tus necesidades personalizadas en DSH: cuando quieres una página o una pequeña herramienta para conservar, propone una nueva página personalizada, que se genera en segundo plano al hacer clic en Crear; cuando preguntas cómo hacer algo en esta aplicación o dices que falta una función, recomienda el complemento integrado que la ofrece, que se activa al hacer clic en Habilitar.\",\"fr\":\"Affiche des cartes de recommandation sous la réponse pour vos besoins personnalisés dans DSH : lorsque vous voulez une page ou un petit outil à conserver, propose une nouvelle page personnalisée, générée en arrière-plan après un clic sur Créer ; lorsque vous demandez comment faire quelque chose dans cette application ou signalez qu’une fonctionnalité manque, recommande le plugin intégré qui la fournit, activé lorsque vous cliquez sur Activer.\",\"ja\":\"DSH に対するあなた個人のニーズに合わせて、回答の下におすすめカードを表示します。長く使えるページや小さなツールが欲しいときは新しいパーソナルページを提案し、「作成」をクリックするとバックグラウンドで生成します。このアプリで何かをする方法を尋ねたときや機能が足りないと言ったときは、それを提供する内蔵プラグインを推薦し、「有効にする」をクリックすると有効になります。\",\"pt\":\"Mostra cartões de recomendação abaixo da resposta para suas necessidades personalizadas no DSH: quando você quer uma página ou uma pequena ferramenta para manter, propõe uma nova página personalizada, gerada em segundo plano depois que você clica em Criar; quando você pergunta como fazer algo neste aplicativo ou diz que falta um recurso, recomenda o plug-in integrado que o oferece, ativado quando você clica em Ativar.\",\"ru\":\"Показывает карточки рекомендаций под ответом с учетом ваших индивидуальных потребностей в DSH: когда вам нужна страница или небольшой инструмент для постоянного использования, предлагает новую персональную страницу, которая создается в фоне после нажатия «Создать»; когда вы спрашиваете, как что-то сделать в этом приложении, или говорите, что не хватает какой-то возможности, рекомендует встроенный плагин, который ее предоставляет, и включает его после нажатия «Включить».\",\"zh\":\"针对你对 DSH 的个性化需求,在回答下方展示推荐卡片:想要能长期使用的页面或小工具时,提议新增个性化页面,点击创建后在后台生成;问到本应用怎么做某件事或缺少某项能力时,推荐对应的内置插件,点击启用后开启。\"},\"enabled\":true},{\"name\":\"@deepseek-ai/dsh-experimental-inspector-profile\",\"title\":{\"en\":\"Developer tools\",\"de\":\"Entwicklertools\",\"es\":\"Herramientas de desarrollador\",\"fr\":\"Outils de développement\",\"ja\":\"開発者ツール\",\"pt\":\"Ferramentas de desenvolvedor\",\"ru\":\"Инструменты разработчика\",\"zh\":\"开发者工具\"},\"description\":{\"en\":\"For troubleshooting: view what DSH recorded for a session in Session log, inspect per-turn and per-step statistics in the Statistics tab, and debug the DSH backend in Chrome DevTools.\",\"de\":\"Zur Fehlersuche: Sehen Sie in „Sitzungsprotokoll“, was DSH für eine Sitzung aufgezeichnet hat, prüfen Sie im Tab „Statistik“ Kontext, Dauer, Cache und Werkzeugkosten pro Runde und Schritt und debuggen Sie das DSH-Backend in Chrome DevTools.\",\"es\":\"Para solucionar problemas: vea en «Registro de sesión» lo que DSH registró de una sesión, consulte el contexto, los tiempos, la caché y el coste de herramientas por turno y paso en la pestaña «Estadísticas», y depure el backend de DSH en Chrome DevTools.\",\"fr\":\"Pour le dépannage : consultez dans « Journal de session » ce que DSH a enregistré pour une session, examinez le contexte, les durées, le cache et le coût des outils par tour et étape dans l’onglet « Statistiques », et déboguez le backend de DSH dans Chrome DevTools.\",\"ja\":\"トラブルシューティング用です。「セッションログ」で DSH がセッションについて記録した内容を確認し、「統計」タブでターン/ステップごとのコンテキスト、時間、キャッシュ、ツールコストを確認し、Chrome DevTools で DSH バックエンドをデバッグできます。\",\"pt\":\"Para solução de problemas: veja em «Registro da sessão» o que o DSH registrou de uma sessão, consulte contexto, tempos, cache e custo de ferramentas por turno e etapa na aba «Estatísticas» e depure o backend do DSH no Chrome DevTools.\",\"ru\":\"Для диагностики: просматривайте в «Журнале сеанса», что DSH записал для сеанса, смотрите контекст, время, кэш и стоимость инструментов по ходам и шагам на вкладке «Статистика» и отлаживайте бэкенд DSH в Chrome DevTools.\",\"zh\":\"用于排查问题:在「会话数据诊断」中查看 DSH 为会话记录的内容,在「统计」页签查看每轮/每步的上下文、耗时、缓存和工具成本,并在 Chrome DevTools 中调试 DSH 后端。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-voice-input-bundle\",\"title\":{\"en\":\"Voice input\",\"de\":\"Spracheingabe\",\"es\":\"Entrada de voz\",\"fr\":\"Saisie vocale\",\"ja\":\"音声入力\",\"pt\":\"Entrada de voz\",\"ru\":\"Голосовой ввод\",\"zh\":\"语音输入\"},\"description\":{\"en\":\"Transcribe recordings locally with SenseVoice; first use requires installing dependencies.\",\"de\":\"Transkribieren Sie Aufnahmen lokal mit SenseVoice; Für die erste Verwendung müssen Abhängigkeiten installiert werden.\",\"es\":\"Transcribe grabaciones localmente con SenseVoice; El primer uso requiere instalar dependencias.\",\"fr\":\"Transcrivez les enregistrements localement avec SenseVoice ; la première utilisation nécessite l'installation de dépendances.\",\"ja\":\"ローカルの SenseVoice で録音を文字に起こします。初回利用時には依存関係のインストールが必要です。\",\"pt\":\"Transcreva gravações localmente com SenseVoice; o primeiro uso requer a instalação de dependências.\",\"ru\":\"Транскрибируйте записи локально с помощью SenseVoice; при первом использовании требуется установка зависимостей.\",\"zh\":\"在本机使用 SenseVoice 转写录音,首次使用需安装依赖。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-auto-review\",\"title\":{\"en\":\"Auto authorization review\",\"de\":\"Automatische Autorisierungsüberprüfung\",\"es\":\"Revisión de autorización automática\",\"fr\":\"Examen des autorisations automatiques\",\"ja\":\"自動認可レビュー\",\"pt\":\"Revisão de autorização automática\",\"ru\":\"Проверка автоматической авторизации\",\"zh\":\"自动授权审查\"},\"description\":{\"en\":\"Adds an Auto review access mode: before each tool call, a model checks it against what you asked for, runs allowed calls at once, and holds denied calls for your approval.\",\"de\":\"Fügt den Zugriffsmodus „Auto review“ hinzu: Vor jedem Tool-Aufruf prüft ein Modell den Aufruf anhand Ihrer Anforderung; zugelassene Aufrufe laufen sofort, abgelehnte Aufrufe warten auf Ihre Genehmigung.\",\"es\":\"Añade el modo de acceso “Auto review”: antes de cada llamada a una herramienta, un modelo comprueba la llamada frente a lo que usted pidió; las llamadas permitidas se ejecutan de inmediato y las denegadas esperan su aprobación.\",\"fr\":\"Ajoute le mode d'accès « Auto review » : avant chaque appel d'outil, un modèle vérifie l'appel par rapport à ce que vous avez demandé ; les appels autorisés s'exécutent immédiatement et les appels refusés attendent votre approbation.\",\"ja\":\"「自動認可レビュー」アクセスモードを追加します。各ツール呼び出しの前に、モデルがその呼び出しをあなたの依頼内容と照らし合わせて確認し、許可された呼び出しは即座に実行され、拒否された呼び出しはあなたの承認を待ちます。\",\"pt\":\"Adiciona o modo de acesso “Auto review”: antes de cada chamada de ferramenta, um modelo verifica a chamada em relação ao que você pediu; as chamadas permitidas são executadas imediatamente e as negadas aguardam sua aprovação.\",\"ru\":\"Добавляет режим доступа «Auto review»: перед каждым вызовом инструмента модель сверяет вызов с вашей просьбой; разрешённые вызовы выполняются сразу, а отклонённые ждут вашего одобрения.\",\"zh\":\"提供“自动审查”访问模式:每次工具调用前,由模型对照你的要求检查,获准的调用立即运行,被拒绝的调用等你审批。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-cot-translation-bundle\",\"title\":{\"en\":\"Machine translation of reasoning\",\"de\":\"Maschinelle Übersetzung der Argumentation\",\"es\":\"Traducción automática de razonamiento\",\"fr\":\"Traduction automatique du raisonnement\",\"ja\":\"思考内容の機械翻訳\",\"pt\":\"Tradução automática de raciocínio\",\"ru\":\"Машинный перевод рассуждений\",\"zh\":\"思考过程机器翻译\"},\"description\":{\"en\":\"Translates expanded Think rows into your chosen language as the agent writes them, and View original switches back.\",\"de\":\"Übersetzt aufgeklappte „Denken Sie nach“-Zeilen, während der Agent sie schreibt, in die von Ihnen gewählte Sprache, und „Original ansehen“ wechselt zurück.\",\"es\":\"Traduce las filas “pensar” expandidas al idioma que elija mientras el agente las escribe, y “Ver original” vuelve al texto original.\",\"fr\":\"Traduit les lignes « Pensez » développées dans la langue de votre choix pendant que l'agent les écrit, et « Voir l'original » permet de revenir au texte d'origine.\",\"ja\":\"エージェントが書き出すのと同時に、展開された「思考」行を選択した言語に翻訳します。「原文を表示」で原文に戻せます。\",\"pt\":\"Traduz as linhas “Pense” expandidas para o idioma que você escolher enquanto o agente as escreve, e “Ver original” volta ao texto original.\",\"ru\":\"Переводит развёрнутые строки «Подумайте» на выбранный вами язык по мере того, как агент их пишет, а «Посмотреть оригинал» возвращает исходный текст.\",\"zh\":\"智能体输出时,把展开的“思考”行翻译成你选择的语言,点击“查看原文”可切换回原文。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-badge-skill-bundle\",\"title\":{\"en\":\"DSH badge skill\",\"de\":\"DSH-Abzeichen-Fertigkeit\",\"es\":\"Habilidad de insignia DSH\",\"fr\":\"Compétence du badge DSH\",\"ja\":\"DSH バッジスキル\",\"pt\":\"DSH habilidade de distintivo\",\"ru\":\"DSH значок навыка\",\"zh\":\"DSH 徽章技能\"},\"description\":{\"en\":\"Let agents add a “powered by dsh” badge to documents, pull requests, and other outputs.\",\"de\":\"Lassen Sie Agenten Dokumente, Pull-Requests und andere Ausgaben mit einem „powered by dsh“-Badge versehen.\",\"es\":\"Permita que los agentes agreguen una insignia \\\"con tecnología de dsh\\\" a documentos, solicitudes de extracción y otros resultados.\",\"fr\":\"Laissez les agents ajouter un badge « propulsé par dsh » aux documents, demandes d'extraction et autres sorties.\",\"ja\":\"エージェントが文書や PR などの成果物に「powered by dsh」バッジを追加できます。\",\"pt\":\"Permita que os agentes adicionem um selo “powered by dsh” a documentos, pull requests e outros resultados.\",\"ru\":\"Позвольте агентам добавлять значок «Powered by dsh» к документам, запросам на включение и другим результатам.\",\"zh\":\"让 Agent 为文档、PR 等产出添加「powered by dsh」徽章。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-ralph-bundle\",\"title\":{\"en\":\"Ralph loops\",\"de\":\"Ralph-Loops\",\"es\":\"Bucles de Ralph\",\"fr\":\"Boucles Ralph\",\"ja\":\"Ralph ループ\",\"pt\":\"Loops de Ralph\",\"ru\":\"Ральф петли\",\"zh\":\"Ralph 循环\"},\"description\":{\"en\":\"Ask for a Ralph loop with a goal, and the agent starts a fresh agent on it round after round until the goal is met, a blocker is reported, or the round limit is reached.\",\"de\":\"Bitten Sie um eine Ralph-Schleife mit einem Ziel, und der Agent startet Runde um Runde einen neuen Agenten darauf, bis das Ziel erreicht ist, ein Hindernis gemeldet wird oder das Rundenlimit erreicht ist.\",\"es\":\"Pida un bucle Ralph con un objetivo, y el agente inicia un agente nuevo sobre él ronda tras ronda hasta que se cumpla el objetivo, se informe de un bloqueo o se alcance el límite de rondas.\",\"fr\":\"Demandez une boucle Ralph avec un objectif, et l'agent lance un nouvel agent dessus, tour après tour, jusqu'à ce que l'objectif soit atteint, qu'un blocage soit signalé ou que la limite de tours soit atteinte.\",\"ja\":\"目標を指定して Ralph ループを依頼すると、エージェントは、目標が達成されるか、阻害要因が報告されるか、ラウンド数の上限に達するまで、ラウンドごとに新しいエージェントを起動してその目標に取り組ませます。\",\"pt\":\"Peça um loop Ralph com um objetivo, e o agente inicia um novo agente nele rodada após rodada até que o objetivo seja atingido, um bloqueio seja informado ou o limite de rodadas seja alcançado.\",\"ru\":\"Попросите запустить цикл Ralph с целью, и агент будет раунд за раундом запускать для неё нового агента, пока цель не будет достигнута, не будет сообщено о препятствии или не будет достигнут лимит раундов.\",\"zh\":\"要求用 Ralph 循环完成一个目标后,智能体会一轮接一轮地启动新的智能体处理它,直到目标达成、报告受阻或达到轮次上限。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-session-titles-bundle\",\"title\":{\"en\":\"Conversation-following titles\",\"de\":\"Konversationsfolgende Titel\",\"es\":\"Títulos que siguen la conversación\",\"fr\":\"Titres de suivi de conversation\",\"ja\":\"会話に追従するタイトル\",\"pt\":\"Títulos que acompanham conversas\",\"ru\":\"Заголовки, следующие за разговором\",\"zh\":\"随对话更新的标题\"},\"description\":{\"en\":\"Writes a session's title from all of your messages and updates it when the main task changes, instead of only from the first message.\",\"de\":\"Erstellt den Titel einer Sitzung aus all Ihren Nachrichten und aktualisiert ihn, wenn sich die Hauptaufgabe ändert, statt ihn nur aus der ersten Nachricht zu bilden.\",\"es\":\"Escribe el título de una sesión a partir de todos sus mensajes y lo actualiza cuando cambia la tarea principal, en lugar de basarse solo en el primer mensaje.\",\"fr\":\"Rédige le titre d'une session à partir de tous vos messages et le met à jour lorsque la tâche principale change, au lieu de se baser uniquement sur le premier message.\",\"ja\":\"最初のメッセージだけでなく、あなたのすべてのメッセージからセッションのタイトルを作成し、主なタスクが変わったときに更新します。\",\"pt\":\"Escreve o título de uma sessão a partir de todas as suas mensagens e o atualiza quando a tarefa principal muda, em vez de usar apenas a primeira mensagem.\",\"ru\":\"Составляет заголовок сеанса по всем вашим сообщениям и обновляет его при смене основной задачи, а не только по первому сообщению.\",\"zh\":\"根据你的所有消息生成会话标题,并在主要任务变化时更新,而不是只根据第一条消息生成。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-terminal-bundle\",\"title\":{\"en\":\"Persistent terminals\",\"de\":\"Persistente Terminals\",\"es\":\"Terminales persistentes\",\"fr\":\"Terminaux persistants\",\"ja\":\"永続ターミナル\",\"pt\":\"Terminais persistentes\",\"ru\":\"Постоянные терминалы\",\"zh\":\"持久终端\"},\"description\":{\"en\":\"Gives the agent terminals whose processes keep running in the background between tool calls, so it can start a dev server or REPL and come back later to read output or type input.\",\"de\":\"Stellt dem Agenten Terminals bereit, deren Prozesse zwischen Tool-Aufrufen im Hintergrund weiterlaufen, sodass er einen Entwicklungsserver oder eine REPL starten und später zurückkehren kann, um Ausgaben zu lesen oder Eingaben zu tippen.\",\"es\":\"Proporciona al agente terminales cuyos procesos siguen ejecutándose en segundo plano entre llamadas a herramientas, de modo que puede iniciar un servidor de desarrollo o un REPL y volver más tarde para leer la salida o escribir entradas.\",\"fr\":\"Fournit à l'agent des terminaux dont les processus continuent de s'exécuter en arrière-plan d'un appel d'outil à l'autre, afin qu'il puisse lancer un serveur de développement ou un REPL et revenir plus tard lire la sortie ou saisir des entrées.\",\"ja\":\"エージェントにターミナルを提供します。プロセスはツール呼び出し間もバックグラウンドで実行され続けるため、エージェントは開発サーバーや REPL を起動し、後から出力を読んだり入力したりできます。\",\"pt\":\"Fornece ao agente terminais cujos processos continuam em execução em segundo plano entre chamadas de ferramentas, para que ele possa iniciar um servidor de desenvolvimento ou um REPL e voltar depois para ler a saída ou digitar entradas.\",\"ru\":\"Предоставляет агенту терминалы, процессы которых продолжают работать в фоне между вызовами инструментов, поэтому агент может запустить сервер разработки или REPL и позже вернуться, чтобы прочитать вывод или ввести данные.\",\"zh\":\"为智能体提供终端,进程在多次工具调用之间持续在后台运行,因此它可以启动开发服务器或 REPL,稍后再读取输出或继续输入。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-liveshare\",\"title\":{\"en\":\"LiveShare Session Sharing\",\"de\":\"LiveShare-Sitzungsfreigabe\",\"es\":\"Compartir sesión de LiveShare\",\"fr\":\"Partage de session LiveShare\",\"ja\":\"LiveShare セッション共有\",\"pt\":\"Compartilhamento de sessão do LiveShare\",\"ru\":\"Совместный доступ к сеансу LiveShare\",\"zh\":\"LiveShare 会话分享\"},\"description\":{\"en\":\"Share the current Session with a colleague, who can drive it in a browser, edit files inside the granted workspace when the share allows writes, or branch their own Session from the completed history.\",\"de\":\"Teilt die aktuelle Sitzung mit einer Kollegin oder einem Kollegen, die oder der sie im Browser bedienen, bei erlaubten Schreibzugriffen Dateien im freigegebenen Arbeitsbereich bearbeiten oder aus der abgeschlossenen Historie eine eigene Sitzung abzweigen kann.\",\"es\":\"Comparte la sesión actual con un compañero, que puede dirigirla en un navegador, editar archivos dentro del espacio de trabajo concedido cuando el recurso compartido permite escritura, o crear su propia sesión a partir del historial completado.\",\"fr\":\"Partagez la session en cours avec un collègue, qui peut la piloter dans un navigateur, modifier des fichiers dans l'espace de travail accordé lorsque le partage autorise l'écriture, ou créer sa propre session à partir de l'historique terminé.\",\"ja\":\"現在のセッションを同僚と共有し、ブラウザから操作してもらう、共有が書き込みを許可する場合は付与されたワークスペース内のファイルを編集してもらう、または完了した履歴から自分のセッションを分岐してもらえます。\",\"pt\":\"Compartilhe a sessão atual com um colega, que pode conduzi-la no navegador, editar arquivos dentro do espaço de trabalho concedido quando o compartilhamento permite escrita, ou ramificar a própria sessão a partir do histórico concluído.\",\"ru\":\"Поделитесь текущим сеансом с коллегой: он сможет управлять им в браузере, редактировать файлы в предоставленном рабочем пространстве, если общий доступ разрешает запись, или ответвить собственный сеанс из завершённой истории.\",\"zh\":\"把当前会话分享给同事,对方可在浏览器中驱动该会话、在分享允许写入时修改授权工作区内的文件,或从已完成历史分支出自己的会话。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-session-search\",\"title\":{\"en\":\"Session search\",\"de\":\"Sitzungssuche\",\"es\":\"búsqueda de sesión\",\"fr\":\"Recherche de sessions\",\"ja\":\"セッション検索\",\"pt\":\"Pesquisa de sessão\",\"ru\":\"Поиск сеанса\",\"zh\":\"会话搜索\"},\"description\":{\"en\":\"Ask about earlier work, such as “How did we fix the pool leak last week?”, and the agent searches your earlier sessions in the same workspace to answer.\",\"de\":\"Fragen Sie nach früherer Arbeit, etwa „Wie haben wir letzte Woche das Pool-Leck behoben?“, und der Agent durchsucht Ihre früheren Sitzungen aus demselben Arbeitsverzeichnis, um zu antworten.\",\"es\":\"Pregunte por trabajo anterior, como “¿Cómo arreglamos la fuga del pool la semana pasada?”, y el agente busca en sus sesiones anteriores del mismo directorio de trabajo para responder.\",\"fr\":\"Posez une question sur un travail antérieur, par exemple « Comment avons-nous corrigé la fuite du pool la semaine dernière ? », et l'agent recherche dans vos sessions antérieures du même répertoire de travail pour répondre.\",\"ja\":\"「先週プールのリークをどう直した?」のように以前の作業について尋ねると、エージェントは同じ作業ディレクトリの過去のセッションを検索して回答します。\",\"pt\":\"Pergunte sobre trabalhos anteriores, como “Como corrigimos o vazamento do pool na semana passada?”, e o agente pesquisa suas sessões anteriores do mesmo diretório de trabalho para responder.\",\"ru\":\"Спросите о прошлой работе, например «Как мы исправили утечку в пуле на прошлой неделе?», и агент выполнит поиск по вашим прошлым сеансам из того же рабочего каталога, чтобы ответить.\",\"zh\":\"询问以往的工作,例如“上周我们是怎么修好连接池泄漏的?”,智能体会搜索同一工作区的历史会话来回答。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-tool-worktree\",\"title\":{\"en\":\"Git worktrees\",\"de\":\"Git-Arbeitsbäume\",\"es\":\"árboles de trabajo de git\",\"fr\":\"Arbres de travail Git\",\"ja\":\"Git ワークツリー\",\"pt\":\"Árvores de trabalho Git\",\"ru\":\"Рабочие деревья Git\",\"zh\":\"Git 工作树\"},\"description\":{\"en\":\"Ask for work on a new branch, and the agent creates a separate Git worktree and continues there, leaving your current checkout as it is.\",\"de\":\"Bitten Sie um Arbeit auf einem neuen Branch, und der Agent erstellt einen separaten Git-Worktree und arbeitet dort weiter, während Ihr aktueller Checkout unverändert bleibt.\",\"es\":\"Pida trabajar en una rama nueva, y el agente crea un worktree de Git independiente y continúa allí, dejando su checkout actual tal como está.\",\"fr\":\"Demandez à travailler sur une nouvelle branche, et l'agent crée un worktree Git distinct et y poursuit le travail, en laissant votre copie de travail actuelle telle quelle.\",\"ja\":\"新しいブランチでの作業を依頼すると、エージェントは別の Git ワークツリーを作成してそこで作業を続け、現在のチェックアウトはそのまま残します。\",\"pt\":\"Peça para trabalhar em um novo branch, e o agente cria um worktree do Git separado e continua lá, deixando seu checkout atual como está.\",\"ru\":\"Попросите поработать в новой ветке, и агент создаст отдельное рабочее дерево Git и продолжит работу там, оставив вашу текущую рабочую копию как есть.\",\"zh\":\"要求在新分支上工作时,智能体会创建单独的 Git 工作树并在其中继续,你当前的工作树保持原样。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-plugin-market-bundle\",\"title\":{\"en\":\"Custom plugin indexes\",\"de\":\"Benutzerdefinierte Plugin-Indizes\",\"es\":\"Índices de complementos personalizados\",\"fr\":\"Index de plugins personnalisés\",\"ja\":\"カスタムプラグインインデックス\",\"pt\":\"Índices de plug-ins personalizados\",\"ru\":\"Пользовательские индексы плагинов\",\"zh\":\"自定义插件索引\"},\"description\":{\"en\":\"Adds a Discover tab to Plugins, where you browse and install plugins from plugin indexes you add.\",\"de\":\"Fügt unter „Plugins“ den Tab „Entdecken“ hinzu, in dem Sie Plugins aus von Ihnen hinzugefügten Plugin-Indizes durchsuchen und installieren.\",\"es\":\"Añade la pestaña “Descubrir” a “Complementos”, donde puede explorar e instalar complementos de los índices de complementos que agregue.\",\"fr\":\"Ajoute à « Plugins » un onglet « Découvrez » où vous parcourez et installez des plugins provenant des index de plugins que vous ajoutez.\",\"ja\":\"「プラグイン」に「見つける」タブを追加し、追加したプラグインインデックスからプラグインを閲覧・インストールできるようにします。\",\"pt\":\"Adiciona a guia “Descubra” a “Plug-ins”, onde você navega e instala plug-ins dos índices de plug-ins que adiciona.\",\"ru\":\"Добавляет в «Плагины» вкладку «Откройте для себя», где можно просматривать и устанавливать плагины из добавленных вами индексов плагинов.\",\"zh\":\"在插件页添加“发现”标签页,从你添加的插件索引中浏览和安装插件。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-forge-bundle\",\"title\":{\"en\":\"Change requests & worktrees\",\"de\":\"Änderungsanfragen und Arbeitsbäume\",\"es\":\"Solicitudes de cambio y árboles de trabajo\",\"fr\":\"Demandes de modification et arbres de travail\",\"ja\":\"変更リクエストとワークツリー\",\"pt\":\"Solicitações de alteração e árvores de trabalho\",\"ru\":\"Запросы на изменение и рабочие деревья\",\"zh\":\"变更请求与工作树\"},\"description\":{\"en\":\"Track each Session's change requests with their checks and reviews, and start new Sessions in a Git worktree; uses the gh CLI for GitHub.\",\"de\":\"Verfolgen Sie die Änderungsanfragen jeder Sitzung mit ihren Prüfungen und Reviews und starten Sie neue Sitzungen in einem Git-Arbeitsbaum; nutzt die gh CLI für GitHub.\",\"es\":\"Siga las solicitudes de cambio de cada sesión con sus comprobaciones y revisiones, e inicie nuevas sesiones en un árbol de trabajo de Git; usa la CLI gh para GitHub.\",\"fr\":\"Suivez les demandes de modification de chaque session avec leurs vérifications et revues, et démarrez de nouvelles sessions dans un arbre de travail Git ; utilise la CLI gh pour GitHub.\",\"ja\":\"各セッションの変更リクエストとそのチェックやレビューを追跡し、新しいセッションを Git ワークツリーで開始できます。GitHub には gh CLI を使用します。\",\"pt\":\"Acompanhe as solicitações de alteração de cada sessão com suas verificações e revisões e inicie novas sessões em uma árvore de trabalho Git; usa a CLI gh para o GitHub.\",\"ru\":\"Отслеживайте запросы на изменение каждого сеанса вместе с их проверками и ревью и начинайте новые сеансы в рабочем дереве Git; для GitHub используется gh CLI.\",\"zh\":\"跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-subagent-claude-code\",\"title\":{\"de\":\"Claude Code-Subagent\",\"en\":\"Claude Code subagent\",\"es\":\"Subagente Claude Code\",\"fr\":\"Sous-agent Claude Code\",\"ja\":\"Claude Code サブエージェント\",\"pt\":\"Claude Code subagente\",\"ru\":\"Субагент Claude Code\",\"zh\":\"Claude Code 子智能体\"},\"description\":{\"de\":\"Delegieren Sie eine Aufgabe an Claude Code, indem Sie dessen native Authentifizierung und Einstellungen verwenden.\",\"en\":\"Delegate one task to Claude Code using its native authentication and settings.\",\"es\":\"Delegue una tarea a Claude Code utilizando su autenticación y configuración nativas.\",\"fr\":\"Déléguez une tâche à Claude Code à l’aide de son authentification et de ses paramètres natifs.\",\"ja\":\"Claude Code のネイティブの認証と設定を使用して、単独のタスクを委派します。\",\"pt\":\"Delegue uma tarefa a Claude Code usando sua autenticação e configurações nativas.\",\"ru\":\"Делегируйте одну задачу Claude Code, используя его встроенную аутентификацию и настройки.\",\"zh\":\"使用 Claude Code 原生认证和设置委派单个任务。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-subagent-codex\",\"title\":{\"de\":\"Codex-Subagent\",\"en\":\"Codex subagent\",\"es\":\"Subagente Codex\",\"fr\":\"Sous-agent Codex\",\"ja\":\"Codex サブエージェント\",\"pt\":\"Codex subagente\",\"ru\":\"Субагент Codex\",\"zh\":\"Codex 子智能体\"},\"description\":{\"de\":\"Delegieren Sie eine Aufgabe an Codex, indem Sie dessen native Authentifizierung und Einstellungen verwenden.\",\"en\":\"Delegate one task to Codex using its native authentication and settings.\",\"es\":\"Delegue una tarea a Codex utilizando su autenticación y configuración nativas.\",\"fr\":\"Déléguez une tâche à Codex à l’aide de son authentification et de ses paramètres natifs.\",\"ja\":\"Codex のネイティブの認証と設定を使用して、単独のタスクを委派します。\",\"pt\":\"Delegue uma tarefa a Codex usando sua autenticação e configurações nativas.\",\"ru\":\"Делегируйте одну задачу Codex, используя его встроенную аутентификацию и настройки.\",\"zh\":\"使用 Codex 原生认证和设置委派单个任务。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-cua-bundle\",\"title\":{\"de\":\"Computernutzung\",\"en\":\"Computer use\",\"es\":\"Uso de la computadora\",\"fr\":\"Utilisation de l'ordinateur\",\"ja\":\"Computer Use\",\"pt\":\"Uso do computador\",\"ru\":\"Использование компьютера\",\"zh\":\"Computer Use\"},\"description\":{\"de\":\"Unterstützt derzeit macOS; Die Windows-Unterstützung befindet sich in der Entwicklung. Lädt die Computernutzung für bestehende und neue PTC-Sitzungen. Wechseln Sie in den PTC-Modus und erteilen Sie die Berechtigungen für 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á 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ón de pantalla para usarlo.\",\"fr\":\"Prend actuellement en charge macOS ; La prise en charge de Windows est en cours de développement. Charge l'utilisation de l'ordinateur pour les sessions PTC existantes et nouvelles. Passez en mode PTC et accordez les autorisations d’accessibilité et d’enregistrement d’écran pour l’utiliser.\",\"ja\":\"現在は macOS のみ対応し、Windows 対応は開発中です。既存および新規の PTC セッションに Computer Use を読み込みます。PTC モードに切り替え、アクセシビリティと画面収録の権限を許可して使用してください。\",\"pt\":\"Atualmente suporta macOS; O suporte Windows está em desenvolvimento. Carrega o uso do computador para sessões PTC novas e existentes. Mude para o modo PTC e conceda permissões de acessibilidade e gravação de tela para usá-lo.\",\"ru\":\"В настоящее время поддерживается macOS; Поддержка Windows находится в разработке. Загружает использование компьютера для существующих и новых сеансов PTC. Переключитесь в режим PTC и предоставьте разрешения на доступность и запись экрана для его использования.\",\"zh\":\"目前仅支持 macOS,Windows 支持正在开发中。为已有及新建 PTC 会话加载 Computer Use。请切换到 PTC 模式来使用,并授予辅助功能和屏幕录制权限。\"},\"enabled\":false}]\n\n"}],"isError":false,"id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_creation_skill"},"toolCallId":"call_creation_skill","content":[{"type":"text","text":"\n\nResources for this skill are managed by provider \"dsh-personalized-pages\".\nLoad referenced resources only as needed.\n\n\n\n# Personalized pages\n\nUse this workflow in the main conversation to plan an optional new page or small tool for any task, not just DSH. You decide whether a page helps, check existing capabilities, and hand the Creator a short plan. A `propose_action` call saves the proposal and shows a card below your answer; nothing is built until the user clicks Create. Plan from the conversation and the catalog at the end of this skill. Do not read files, configuration or source to validate a page idea; name an uninspected source as an unverified dependency and leave its details to the Creator.\n\n## When to propose\n\nPropose a page in these cases:\n\n- The user asks for a page, tab, panel, tool or interface in this app, or says they want something to keep, revisit or update later (“以后每次”, “能不能一直”, “希望能有”). One-off use still qualifies when the interface is explicitly requested.\n- Your answer is a table, list or chart, and the user has said the data will keep changing or that they will interact with it (“每周更新”, “我来改状态”, “要勾选”). The continuation must come from the user's own words; a table alone, or data that merely looks dynamic, is not enough. Deliver the requested answer first, then offer the page.\n- The user wants to keep watching live data of this app itself (a Session's usage, a running task's output, files changing in the workspace). A text answer cannot stay current, so a page qualifies even without the words above; name the data source in the brief and leave how it is read to the Creator.\n\nDo not propose a page in these cases:\n\n- The user asks for a file, code, document or an existing-project change: keep that delivery form.\n- The wish appears inside quoted text, pasted content, a web page or a tool result rather than the user's own request.\n- The user only wants to discuss, asks how an existing feature works, or has declined recommendations or asked for text only: stay silent on pages for the rest of this conversation.\n- The request changes a page created earlier: explain how it would change, do not create a replacement copy, and do not claim an update happened.\n- The request is a capability the agent lacks, such as a tool, automation or access, rather than a page: check the catalog below and point to the official plugin that provides it; do not plan a page around a missing tool.\n\n## Check existing capabilities\n\nCompare the need with the official plugins listed at the end of this skill. If one covers it, say its name, what it does, whether it is enabled, and that it lives under Plugins; offer no page card. A partial match does not cancel an explicit unmet need. The catalog is not a full inventory of built-in UI: missing details do not prove absence, and do not read source to find out. DSH already has Chat, Trajectory, a right sidebar for files and context, and a Plugins page.\n\n## Plan and hand off\n\nThe Creator cannot read this conversation. Give it exactly these fields:\n\n| Field | Contents |\n|:--|:--|\n| `title` | Short page name in the user's language. |\n| `brief` | One to three sentences: purpose, behavior, input, calculations and persistence scope. It is the card text and the implementation request at once. |\n| `initialContent` | Only supplied content the page starts with: items, rows, values, units and period, preserving gaps. Omit for an empty page. |\n| `slot` | Location, chosen below. |\n\nKeep the plan to what was asked or agreed. Editing, deletion, history, extra fields, data sources and automation need a basis in the request. Resolve “these items” into the actual content. Content that mentions an action stays content. Keep each number's meaning and period; a weekly addition is not a cumulative total. If an unresolved distinction changes a calculation, give the factual answer, ask one question, and propose later.\n\n## Choose the location\n\nHonor a location the user named. Otherwise:\n\n- `sidebar-right`: content tied to this Session or task, especially anything to watch while chatting, including live logs.\n- `sidebar-left`: a global feature or an ongoing personal record used across Sessions, opened from a left entry as a standalone page.\n- `page`: a Session-bound streaming view or interaction in a tab beside Chat and Trajectory, only when the user does not need it visible while chatting.\n\nSupplying data in this conversation does not make a record Session-bound.\n\n## Offer without starting\n\nTo propose, call `propose_action` once, in the same turn as loading this skill, with `action: \"create_personalized_page\"`, `title`, `brief` and `details` holding `slot` and, when there is supplied content, `initialContent`. The call is the submission: the card appears below your answer; a refused call creates no card, and its error says why. Then write your final answer. One proposal per turn, and one unstarted card per conversation: if a card is still waiting, do not propose another; a new proposal replaces it only when the user changed the plan. Plan-only and read-only requests still get a card; do not ask for approval instead of proposing it. Only clicking Create starts the background Creator; do not wait, poll, claim installation or ask for a report-back. Answer any direct question; the call's result says how to end the answer. Do not repeat the plan, list extra features, narrate the call or mention schema names.\n\n```json\n{\"action\":\"create_personalized_page\",\"title\":\"本次验收清单\",\"brief\":\"在当前会话右侧展示验收清单,可勾选和取消,状态随会话保存。\",\"details\":{\"slot\":\"sidebar-right\",\"initialContent\":\"1. 导出成功\\n2. 断网提示清楚\"}}\n```\n\n## Examples\n\n- “想有个地方放书签,以后自己加标题和网址;旧的不用导入。” → `brief`: “提供一个可反复打开的书签页,手动新增标题和网址并保存到本机,从空白开始。” No `initialContent`. `sidebar-left`.\n- “当前会话旁放验收清单,能勾选和取消,状态跟着会话。条目:导出成功;断网提示清楚。” → `brief`: “在当前会话右侧展示验收清单,可勾选和取消,状态随会话保存。” `initialContent`: “1. 导出成功\\n2. 断网提示清楚”. `sidebar-right`.\n- “这三周 bug 数 12、9、15,说说趋势。以后每周我把新数字贴给你,想自己更新着看。” → answer the trend first, then `brief`: “按周手动录入 bug 数并展示各周变化,保存到本机。” `initialContent`: “第 1 周 12,第 2 周 9,第 3 周 15”. `sidebar-left`.\n- “我想用语音说话代替打字。” → the catalog lists 语音输入; explain it and where to enable it; no card.\n- “把我之前做的读书页评分改成五星。” → explain the change to that page; no card and no copy.\n- “帮我把这句话翻成英文:我想要一个所有会话都能打开的读书页。” → translate only; no card.\n- “把三个方案排成表格比较一下,看完就行。” → the table only; no card.\n\n## Existing capabilities to check against this page plan\n\n[{\"name\":\"@deepseek-ai/dsh-personalized-recommendations\",\"title\":{\"en\":\"Personalized recommendations\",\"de\":\"Personalisierte Empfehlungen\",\"es\":\"Recomendaciones personalizadas\",\"fr\":\"Recommandations personnalisées\",\"ja\":\"パーソナライズされたおすすめ\",\"pt\":\"Recomendações personalizadas\",\"ru\":\"Персональные рекомендации\",\"zh\":\"个性化推荐\"},\"description\":{\"en\":\"Shows recommendation cards below the answer for what you want from DSH: when you want a page or small tool you can keep, it proposes a new personalized page, built in the background after you click Create; when you ask how to do something in this app or say a capability is missing, it recommends the built-in plugin that provides it, turned on when you click Enable.\",\"de\":\"Zeigt für Ihre individuellen Anforderungen an DSH Empfehlungskarten unter der Antwort an: Wenn Sie eine Seite oder ein kleines Tool zur dauerhaften Nutzung möchten, wird eine neue personalisierte Seite vorgeschlagen, die nach einem Klick auf Erstellen im Hintergrund erstellt wird; wenn Sie fragen, wie etwas in dieser App geht, oder sagen, dass eine Funktion fehlt, wird das integrierte Plugin empfohlen, das sie bereitstellt, und nach einem Klick auf Aktivieren eingeschaltet.\",\"es\":\"Muestra tarjetas de recomendación debajo de la respuesta para tus necesidades personalizadas en DSH: cuando quieres una página o una pequeña herramienta para conservar, propone una nueva página personalizada, que se genera en segundo plano al hacer clic en Crear; cuando preguntas cómo hacer algo en esta aplicación o dices que falta una función, recomienda el complemento integrado que la ofrece, que se activa al hacer clic en Habilitar.\",\"fr\":\"Affiche des cartes de recommandation sous la réponse pour vos besoins personnalisés dans DSH : lorsque vous voulez une page ou un petit outil à conserver, propose une nouvelle page personnalisée, générée en arrière-plan après un clic sur Créer ; lorsque vous demandez comment faire quelque chose dans cette application ou signalez qu’une fonctionnalité manque, recommande le plugin intégré qui la fournit, activé lorsque vous cliquez sur Activer.\",\"ja\":\"DSH に対するあなた個人のニーズに合わせて、回答の下におすすめカードを表示します。長く使えるページや小さなツールが欲しいときは新しいパーソナルページを提案し、「作成」をクリックするとバックグラウンドで生成します。このアプリで何かをする方法を尋ねたときや機能が足りないと言ったときは、それを提供する内蔵プラグインを推薦し、「有効にする」をクリックすると有効になります。\",\"pt\":\"Mostra cartões de recomendação abaixo da resposta para suas necessidades personalizadas no DSH: quando você quer uma página ou uma pequena ferramenta para manter, propõe uma nova página personalizada, gerada em segundo plano depois que você clica em Criar; quando você pergunta como fazer algo neste aplicativo ou diz que falta um recurso, recomenda o plug-in integrado que o oferece, ativado quando você clica em Ativar.\",\"ru\":\"Показывает карточки рекомендаций под ответом с учетом ваших индивидуальных потребностей в DSH: когда вам нужна страница или небольшой инструмент для постоянного использования, предлагает новую персональную страницу, которая создается в фоне после нажатия «Создать»; когда вы спрашиваете, как что-то сделать в этом приложении, или говорите, что не хватает какой-то возможности, рекомендует встроенный плагин, который ее предоставляет, и включает его после нажатия «Включить».\",\"zh\":\"针对你对 DSH 的个性化需求,在回答下方展示推荐卡片:想要能长期使用的页面或小工具时,提议新增个性化页面,点击创建后在后台生成;问到本应用怎么做某件事或缺少某项能力时,推荐对应的内置插件,点击启用后开启。\"},\"enabled\":true},{\"name\":\"@deepseek-ai/dsh-experimental-inspector-profile\",\"title\":{\"en\":\"Developer tools\",\"de\":\"Entwicklertools\",\"es\":\"Herramientas de desarrollador\",\"fr\":\"Outils de développement\",\"ja\":\"開発者ツール\",\"pt\":\"Ferramentas de desenvolvedor\",\"ru\":\"Инструменты разработчика\",\"zh\":\"开发者工具\"},\"description\":{\"en\":\"For troubleshooting: view what DSH recorded for a session in Session log, inspect per-turn and per-step statistics in the Statistics tab, and debug the DSH backend in Chrome DevTools.\",\"de\":\"Zur Fehlersuche: Sehen Sie in „Sitzungsprotokoll“, was DSH für eine Sitzung aufgezeichnet hat, prüfen Sie im Tab „Statistik“ Kontext, Dauer, Cache und Werkzeugkosten pro Runde und Schritt und debuggen Sie das DSH-Backend in Chrome DevTools.\",\"es\":\"Para solucionar problemas: vea en «Registro de sesión» lo que DSH registró de una sesión, consulte el contexto, los tiempos, la caché y el coste de herramientas por turno y paso en la pestaña «Estadísticas», y depure el backend de DSH en Chrome DevTools.\",\"fr\":\"Pour le dépannage : consultez dans « Journal de session » ce que DSH a enregistré pour une session, examinez le contexte, les durées, le cache et le coût des outils par tour et étape dans l’onglet « Statistiques », et déboguez le backend de DSH dans Chrome DevTools.\",\"ja\":\"トラブルシューティング用です。「セッションログ」で DSH がセッションについて記録した内容を確認し、「統計」タブでターン/ステップごとのコンテキスト、時間、キャッシュ、ツールコストを確認し、Chrome DevTools で DSH バックエンドをデバッグできます。\",\"pt\":\"Para solução de problemas: veja em «Registro da sessão» o que o DSH registrou de uma sessão, consulte contexto, tempos, cache e custo de ferramentas por turno e etapa na aba «Estatísticas» e depure o backend do DSH no Chrome DevTools.\",\"ru\":\"Для диагностики: просматривайте в «Журнале сеанса», что DSH записал для сеанса, смотрите контекст, время, кэш и стоимость инструментов по ходам и шагам на вкладке «Статистика» и отлаживайте бэкенд DSH в Chrome DevTools.\",\"zh\":\"用于排查问题:在「会话数据诊断」中查看 DSH 为会话记录的内容,在「统计」页签查看每轮/每步的上下文、耗时、缓存和工具成本,并在 Chrome DevTools 中调试 DSH 后端。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-voice-input-bundle\",\"title\":{\"en\":\"Voice input\",\"de\":\"Spracheingabe\",\"es\":\"Entrada de voz\",\"fr\":\"Saisie vocale\",\"ja\":\"音声入力\",\"pt\":\"Entrada de voz\",\"ru\":\"Голосовой ввод\",\"zh\":\"语音输入\"},\"description\":{\"en\":\"Transcribe recordings locally with SenseVoice; first use requires installing dependencies.\",\"de\":\"Transkribieren Sie Aufnahmen lokal mit SenseVoice; Für die erste Verwendung müssen Abhängigkeiten installiert werden.\",\"es\":\"Transcribe grabaciones localmente con SenseVoice; El primer uso requiere instalar dependencias.\",\"fr\":\"Transcrivez les enregistrements localement avec SenseVoice ; la première utilisation nécessite l'installation de dépendances.\",\"ja\":\"ローカルの SenseVoice で録音を文字に起こします。初回利用時には依存関係のインストールが必要です。\",\"pt\":\"Transcreva gravações localmente com SenseVoice; o primeiro uso requer a instalação de dependências.\",\"ru\":\"Транскрибируйте записи локально с помощью SenseVoice; при первом использовании требуется установка зависимостей.\",\"zh\":\"在本机使用 SenseVoice 转写录音,首次使用需安装依赖。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-auto-review\",\"title\":{\"en\":\"Auto authorization review\",\"de\":\"Automatische Autorisierungsüberprüfung\",\"es\":\"Revisión de autorización automática\",\"fr\":\"Examen des autorisations automatiques\",\"ja\":\"自動認可レビュー\",\"pt\":\"Revisão de autorização automática\",\"ru\":\"Проверка автоматической авторизации\",\"zh\":\"自动授权审查\"},\"description\":{\"en\":\"Adds an Auto review access mode: before each tool call, a model checks it against what you asked for, runs allowed calls at once, and holds denied calls for your approval.\",\"de\":\"Fügt den Zugriffsmodus „Auto review“ hinzu: Vor jedem Tool-Aufruf prüft ein Modell den Aufruf anhand Ihrer Anforderung; zugelassene Aufrufe laufen sofort, abgelehnte Aufrufe warten auf Ihre Genehmigung.\",\"es\":\"Añade el modo de acceso “Auto review”: antes de cada llamada a una herramienta, un modelo comprueba la llamada frente a lo que usted pidió; las llamadas permitidas se ejecutan de inmediato y las denegadas esperan su aprobación.\",\"fr\":\"Ajoute le mode d'accès « Auto review » : avant chaque appel d'outil, un modèle vérifie l'appel par rapport à ce que vous avez demandé ; les appels autorisés s'exécutent immédiatement et les appels refusés attendent votre approbation.\",\"ja\":\"「自動認可レビュー」アクセスモードを追加します。各ツール呼び出しの前に、モデルがその呼び出しをあなたの依頼内容と照らし合わせて確認し、許可された呼び出しは即座に実行され、拒否された呼び出しはあなたの承認を待ちます。\",\"pt\":\"Adiciona o modo de acesso “Auto review”: antes de cada chamada de ferramenta, um modelo verifica a chamada em relação ao que você pediu; as chamadas permitidas são executadas imediatamente e as negadas aguardam sua aprovação.\",\"ru\":\"Добавляет режим доступа «Auto review»: перед каждым вызовом инструмента модель сверяет вызов с вашей просьбой; разрешённые вызовы выполняются сразу, а отклонённые ждут вашего одобрения.\",\"zh\":\"提供“自动审查”访问模式:每次工具调用前,由模型对照你的要求检查,获准的调用立即运行,被拒绝的调用等你审批。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-cot-translation-bundle\",\"title\":{\"en\":\"Machine translation of reasoning\",\"de\":\"Maschinelle Übersetzung der Argumentation\",\"es\":\"Traducción automática de razonamiento\",\"fr\":\"Traduction automatique du raisonnement\",\"ja\":\"思考内容の機械翻訳\",\"pt\":\"Tradução automática de raciocínio\",\"ru\":\"Машинный перевод рассуждений\",\"zh\":\"思考过程机器翻译\"},\"description\":{\"en\":\"Translates expanded Think rows into your chosen language as the agent writes them, and View original switches back.\",\"de\":\"Übersetzt aufgeklappte „Denken Sie nach“-Zeilen, während der Agent sie schreibt, in die von Ihnen gewählte Sprache, und „Original ansehen“ wechselt zurück.\",\"es\":\"Traduce las filas “pensar” expandidas al idioma que elija mientras el agente las escribe, y “Ver original” vuelve al texto original.\",\"fr\":\"Traduit les lignes « Pensez » développées dans la langue de votre choix pendant que l'agent les écrit, et « Voir l'original » permet de revenir au texte d'origine.\",\"ja\":\"エージェントが書き出すのと同時に、展開された「思考」行を選択した言語に翻訳します。「原文を表示」で原文に戻せます。\",\"pt\":\"Traduz as linhas “Pense” expandidas para o idioma que você escolher enquanto o agente as escreve, e “Ver original” volta ao texto original.\",\"ru\":\"Переводит развёрнутые строки «Подумайте» на выбранный вами язык по мере того, как агент их пишет, а «Посмотреть оригинал» возвращает исходный текст.\",\"zh\":\"智能体输出时,把展开的“思考”行翻译成你选择的语言,点击“查看原文”可切换回原文。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-badge-skill-bundle\",\"title\":{\"en\":\"DSH badge skill\",\"de\":\"DSH-Abzeichen-Fertigkeit\",\"es\":\"Habilidad de insignia DSH\",\"fr\":\"Compétence du badge DSH\",\"ja\":\"DSH バッジスキル\",\"pt\":\"DSH habilidade de distintivo\",\"ru\":\"DSH значок навыка\",\"zh\":\"DSH 徽章技能\"},\"description\":{\"en\":\"Let agents add a “powered by dsh” badge to documents, pull requests, and other outputs.\",\"de\":\"Lassen Sie Agenten Dokumente, Pull-Requests und andere Ausgaben mit einem „powered by dsh“-Badge versehen.\",\"es\":\"Permita que los agentes agreguen una insignia \\\"con tecnología de dsh\\\" a documentos, solicitudes de extracción y otros resultados.\",\"fr\":\"Laissez les agents ajouter un badge « propulsé par dsh » aux documents, demandes d'extraction et autres sorties.\",\"ja\":\"エージェントが文書や PR などの成果物に「powered by dsh」バッジを追加できます。\",\"pt\":\"Permita que os agentes adicionem um selo “powered by dsh” a documentos, pull requests e outros resultados.\",\"ru\":\"Позвольте агентам добавлять значок «Powered by dsh» к документам, запросам на включение и другим результатам.\",\"zh\":\"让 Agent 为文档、PR 等产出添加「powered by dsh」徽章。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-ralph-bundle\",\"title\":{\"en\":\"Ralph loops\",\"de\":\"Ralph-Loops\",\"es\":\"Bucles de Ralph\",\"fr\":\"Boucles Ralph\",\"ja\":\"Ralph ループ\",\"pt\":\"Loops de Ralph\",\"ru\":\"Ральф петли\",\"zh\":\"Ralph 循环\"},\"description\":{\"en\":\"Ask for a Ralph loop with a goal, and the agent starts a fresh agent on it round after round until the goal is met, a blocker is reported, or the round limit is reached.\",\"de\":\"Bitten Sie um eine Ralph-Schleife mit einem Ziel, und der Agent startet Runde um Runde einen neuen Agenten darauf, bis das Ziel erreicht ist, ein Hindernis gemeldet wird oder das Rundenlimit erreicht ist.\",\"es\":\"Pida un bucle Ralph con un objetivo, y el agente inicia un agente nuevo sobre él ronda tras ronda hasta que se cumpla el objetivo, se informe de un bloqueo o se alcance el límite de rondas.\",\"fr\":\"Demandez une boucle Ralph avec un objectif, et l'agent lance un nouvel agent dessus, tour après tour, jusqu'à ce que l'objectif soit atteint, qu'un blocage soit signalé ou que la limite de tours soit atteinte.\",\"ja\":\"目標を指定して Ralph ループを依頼すると、エージェントは、目標が達成されるか、阻害要因が報告されるか、ラウンド数の上限に達するまで、ラウンドごとに新しいエージェントを起動してその目標に取り組ませます。\",\"pt\":\"Peça um loop Ralph com um objetivo, e o agente inicia um novo agente nele rodada após rodada até que o objetivo seja atingido, um bloqueio seja informado ou o limite de rodadas seja alcançado.\",\"ru\":\"Попросите запустить цикл Ralph с целью, и агент будет раунд за раундом запускать для неё нового агента, пока цель не будет достигнута, не будет сообщено о препятствии или не будет достигнут лимит раундов.\",\"zh\":\"要求用 Ralph 循环完成一个目标后,智能体会一轮接一轮地启动新的智能体处理它,直到目标达成、报告受阻或达到轮次上限。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-session-titles-bundle\",\"title\":{\"en\":\"Conversation-following titles\",\"de\":\"Konversationsfolgende Titel\",\"es\":\"Títulos que siguen la conversación\",\"fr\":\"Titres de suivi de conversation\",\"ja\":\"会話に追従するタイトル\",\"pt\":\"Títulos que acompanham conversas\",\"ru\":\"Заголовки, следующие за разговором\",\"zh\":\"随对话更新的标题\"},\"description\":{\"en\":\"Writes a session's title from all of your messages and updates it when the main task changes, instead of only from the first message.\",\"de\":\"Erstellt den Titel einer Sitzung aus all Ihren Nachrichten und aktualisiert ihn, wenn sich die Hauptaufgabe ändert, statt ihn nur aus der ersten Nachricht zu bilden.\",\"es\":\"Escribe el título de una sesión a partir de todos sus mensajes y lo actualiza cuando cambia la tarea principal, en lugar de basarse solo en el primer mensaje.\",\"fr\":\"Rédige le titre d'une session à partir de tous vos messages et le met à jour lorsque la tâche principale change, au lieu de se baser uniquement sur le premier message.\",\"ja\":\"最初のメッセージだけでなく、あなたのすべてのメッセージからセッションのタイトルを作成し、主なタスクが変わったときに更新します。\",\"pt\":\"Escreve o título de uma sessão a partir de todas as suas mensagens e o atualiza quando a tarefa principal muda, em vez de usar apenas a primeira mensagem.\",\"ru\":\"Составляет заголовок сеанса по всем вашим сообщениям и обновляет его при смене основной задачи, а не только по первому сообщению.\",\"zh\":\"根据你的所有消息生成会话标题,并在主要任务变化时更新,而不是只根据第一条消息生成。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-terminal-bundle\",\"title\":{\"en\":\"Persistent terminals\",\"de\":\"Persistente Terminals\",\"es\":\"Terminales persistentes\",\"fr\":\"Terminaux persistants\",\"ja\":\"永続ターミナル\",\"pt\":\"Terminais persistentes\",\"ru\":\"Постоянные терминалы\",\"zh\":\"持久终端\"},\"description\":{\"en\":\"Gives the agent terminals whose processes keep running in the background between tool calls, so it can start a dev server or REPL and come back later to read output or type input.\",\"de\":\"Stellt dem Agenten Terminals bereit, deren Prozesse zwischen Tool-Aufrufen im Hintergrund weiterlaufen, sodass er einen Entwicklungsserver oder eine REPL starten und später zurückkehren kann, um Ausgaben zu lesen oder Eingaben zu tippen.\",\"es\":\"Proporciona al agente terminales cuyos procesos siguen ejecutándose en segundo plano entre llamadas a herramientas, de modo que puede iniciar un servidor de desarrollo o un REPL y volver más tarde para leer la salida o escribir entradas.\",\"fr\":\"Fournit à l'agent des terminaux dont les processus continuent de s'exécuter en arrière-plan d'un appel d'outil à l'autre, afin qu'il puisse lancer un serveur de développement ou un REPL et revenir plus tard lire la sortie ou saisir des entrées.\",\"ja\":\"エージェントにターミナルを提供します。プロセスはツール呼び出し間もバックグラウンドで実行され続けるため、エージェントは開発サーバーや REPL を起動し、後から出力を読んだり入力したりできます。\",\"pt\":\"Fornece ao agente terminais cujos processos continuam em execução em segundo plano entre chamadas de ferramentas, para que ele possa iniciar um servidor de desenvolvimento ou um REPL e voltar depois para ler a saída ou digitar entradas.\",\"ru\":\"Предоставляет агенту терминалы, процессы которых продолжают работать в фоне между вызовами инструментов, поэтому агент может запустить сервер разработки или REPL и позже вернуться, чтобы прочитать вывод или ввести данные.\",\"zh\":\"为智能体提供终端,进程在多次工具调用之间持续在后台运行,因此它可以启动开发服务器或 REPL,稍后再读取输出或继续输入。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-liveshare\",\"title\":{\"en\":\"LiveShare Session Sharing\",\"de\":\"LiveShare-Sitzungsfreigabe\",\"es\":\"Compartir sesión de LiveShare\",\"fr\":\"Partage de session LiveShare\",\"ja\":\"LiveShare セッション共有\",\"pt\":\"Compartilhamento de sessão do LiveShare\",\"ru\":\"Совместный доступ к сеансу LiveShare\",\"zh\":\"LiveShare 会话分享\"},\"description\":{\"en\":\"Share the current Session with a colleague, who can drive it in a browser, edit files inside the granted workspace when the share allows writes, or branch their own Session from the completed history.\",\"de\":\"Teilt die aktuelle Sitzung mit einer Kollegin oder einem Kollegen, die oder der sie im Browser bedienen, bei erlaubten Schreibzugriffen Dateien im freigegebenen Arbeitsbereich bearbeiten oder aus der abgeschlossenen Historie eine eigene Sitzung abzweigen kann.\",\"es\":\"Comparte la sesión actual con un compañero, que puede dirigirla en un navegador, editar archivos dentro del espacio de trabajo concedido cuando el recurso compartido permite escritura, o crear su propia sesión a partir del historial completado.\",\"fr\":\"Partagez la session en cours avec un collègue, qui peut la piloter dans un navigateur, modifier des fichiers dans l'espace de travail accordé lorsque le partage autorise l'écriture, ou créer sa propre session à partir de l'historique terminé.\",\"ja\":\"現在のセッションを同僚と共有し、ブラウザから操作してもらう、共有が書き込みを許可する場合は付与されたワークスペース内のファイルを編集してもらう、または完了した履歴から自分のセッションを分岐してもらえます。\",\"pt\":\"Compartilhe a sessão atual com um colega, que pode conduzi-la no navegador, editar arquivos dentro do espaço de trabalho concedido quando o compartilhamento permite escrita, ou ramificar a própria sessão a partir do histórico concluído.\",\"ru\":\"Поделитесь текущим сеансом с коллегой: он сможет управлять им в браузере, редактировать файлы в предоставленном рабочем пространстве, если общий доступ разрешает запись, или ответвить собственный сеанс из завершённой истории.\",\"zh\":\"把当前会话分享给同事,对方可在浏览器中驱动该会话、在分享允许写入时修改授权工作区内的文件,或从已完成历史分支出自己的会话。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-session-search\",\"title\":{\"en\":\"Session search\",\"de\":\"Sitzungssuche\",\"es\":\"búsqueda de sesión\",\"fr\":\"Recherche de sessions\",\"ja\":\"セッション検索\",\"pt\":\"Pesquisa de sessão\",\"ru\":\"Поиск сеанса\",\"zh\":\"会话搜索\"},\"description\":{\"en\":\"Ask about earlier work, such as “How did we fix the pool leak last week?”, and the agent searches your earlier sessions in the same workspace to answer.\",\"de\":\"Fragen Sie nach früherer Arbeit, etwa „Wie haben wir letzte Woche das Pool-Leck behoben?“, und der Agent durchsucht Ihre früheren Sitzungen aus demselben Arbeitsverzeichnis, um zu antworten.\",\"es\":\"Pregunte por trabajo anterior, como “¿Cómo arreglamos la fuga del pool la semana pasada?”, y el agente busca en sus sesiones anteriores del mismo directorio de trabajo para responder.\",\"fr\":\"Posez une question sur un travail antérieur, par exemple « Comment avons-nous corrigé la fuite du pool la semaine dernière ? », et l'agent recherche dans vos sessions antérieures du même répertoire de travail pour répondre.\",\"ja\":\"「先週プールのリークをどう直した?」のように以前の作業について尋ねると、エージェントは同じ作業ディレクトリの過去のセッションを検索して回答します。\",\"pt\":\"Pergunte sobre trabalhos anteriores, como “Como corrigimos o vazamento do pool na semana passada?”, e o agente pesquisa suas sessões anteriores do mesmo diretório de trabalho para responder.\",\"ru\":\"Спросите о прошлой работе, например «Как мы исправили утечку в пуле на прошлой неделе?», и агент выполнит поиск по вашим прошлым сеансам из того же рабочего каталога, чтобы ответить.\",\"zh\":\"询问以往的工作,例如“上周我们是怎么修好连接池泄漏的?”,智能体会搜索同一工作区的历史会话来回答。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-tool-worktree\",\"title\":{\"en\":\"Git worktrees\",\"de\":\"Git-Arbeitsbäume\",\"es\":\"árboles de trabajo de git\",\"fr\":\"Arbres de travail Git\",\"ja\":\"Git ワークツリー\",\"pt\":\"Árvores de trabalho Git\",\"ru\":\"Рабочие деревья Git\",\"zh\":\"Git 工作树\"},\"description\":{\"en\":\"Ask for work on a new branch, and the agent creates a separate Git worktree and continues there, leaving your current checkout as it is.\",\"de\":\"Bitten Sie um Arbeit auf einem neuen Branch, und der Agent erstellt einen separaten Git-Worktree und arbeitet dort weiter, während Ihr aktueller Checkout unverändert bleibt.\",\"es\":\"Pida trabajar en una rama nueva, y el agente crea un worktree de Git independiente y continúa allí, dejando su checkout actual tal como está.\",\"fr\":\"Demandez à travailler sur une nouvelle branche, et l'agent crée un worktree Git distinct et y poursuit le travail, en laissant votre copie de travail actuelle telle quelle.\",\"ja\":\"新しいブランチでの作業を依頼すると、エージェントは別の Git ワークツリーを作成してそこで作業を続け、現在のチェックアウトはそのまま残します。\",\"pt\":\"Peça para trabalhar em um novo branch, e o agente cria um worktree do Git separado e continua lá, deixando seu checkout atual como está.\",\"ru\":\"Попросите поработать в новой ветке, и агент создаст отдельное рабочее дерево Git и продолжит работу там, оставив вашу текущую рабочую копию как есть.\",\"zh\":\"要求在新分支上工作时,智能体会创建单独的 Git 工作树并在其中继续,你当前的工作树保持原样。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-cua-bundle\",\"title\":{\"en\":\"Computer use\",\"de\":\"Computernutzung\",\"es\":\"Uso de la computadora\",\"fr\":\"Utilisation de l'ordinateur\",\"ja\":\"Computer Use\",\"pt\":\"Uso do computador\",\"ru\":\"Использование компьютера\",\"zh\":\"Computer Use\"},\"description\":{\"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.\",\"de\":\"Unterstützt derzeit macOS; Die Windows-Unterstützung befindet sich in der Entwicklung. Lädt die Computernutzung für bestehende und neue PTC-Sitzungen. Wechseln Sie in den PTC-Modus und erteilen Sie die Berechtigungen für Barrierefreiheit und Bildschirmaufzeichnung, um ihn zu verwenden.\",\"es\":\"Actualmente es compatible con macOS; El soporte para Windows está 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ón de pantalla para usarlo.\",\"fr\":\"Prend actuellement en charge macOS ; La prise en charge de Windows est en cours de développement. Charge l'utilisation de l'ordinateur pour les sessions PTC existantes et nouvelles. Passez en mode PTC et accordez les autorisations d’accessibilité et d’enregistrement d’écran pour l’utiliser.\",\"ja\":\"現在は macOS のみ対応し、Windows 対応は開発中です。既存および新規の PTC セッションに Computer Use を読み込みます。PTC モードに切り替え、アクセシビリティと画面収録の権限を許可して使用してください。\",\"pt\":\"Atualmente suporta macOS; O suporte Windows está em desenvolvimento. Carrega o uso do computador para sessões PTC novas e existentes. Mude para o modo PTC e conceda permissões de acessibilidade e gravação de tela para usá-lo.\",\"ru\":\"В настоящее время поддерживается macOS; Поддержка Windows находится в разработке. Загружает использование компьютера для существующих и новых сеансов PTC. Переключитесь в режим PTC и предоставьте разрешения на доступность и запись экрана для его использования.\",\"zh\":\"目前仅支持 macOS,Windows 支持正在开发中。为已有及新建 PTC 会话加载 Computer Use。请切换到 PTC 模式来使用,并授予辅助功能和屏幕录制权限。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-plugin-market-bundle\",\"title\":{\"en\":\"Custom plugin indexes\",\"de\":\"Benutzerdefinierte Plugin-Indizes\",\"es\":\"Índices de complementos personalizados\",\"fr\":\"Index de plugins personnalisés\",\"ja\":\"カスタムプラグインインデックス\",\"pt\":\"Índices de plug-ins personalizados\",\"ru\":\"Пользовательские индексы плагинов\",\"zh\":\"自定义插件索引\"},\"description\":{\"en\":\"Adds a Discover tab to Plugins, where you browse and install plugins from plugin indexes you add.\",\"de\":\"Fügt unter „Plugins“ den Tab „Entdecken“ hinzu, in dem Sie Plugins aus von Ihnen hinzugefügten Plugin-Indizes durchsuchen und installieren.\",\"es\":\"Añade la pestaña “Descubrir” a “Complementos”, donde puede explorar e instalar complementos de los índices de complementos que agregue.\",\"fr\":\"Ajoute à « Plugins » un onglet « Découvrez » où vous parcourez et installez des plugins provenant des index de plugins que vous ajoutez.\",\"ja\":\"「プラグイン」に「見つける」タブを追加し、追加したプラグインインデックスからプラグインを閲覧・インストールできるようにします。\",\"pt\":\"Adiciona a guia “Descubra” a “Plug-ins”, onde você navega e instala plug-ins dos índices de plug-ins que adiciona.\",\"ru\":\"Добавляет в «Плагины» вкладку «Откройте для себя», где можно просматривать и устанавливать плагины из добавленных вами индексов плагинов.\",\"zh\":\"在插件页添加“发现”标签页,从你添加的插件索引中浏览和安装插件。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-experimental-forge-bundle\",\"title\":{\"en\":\"Change requests & worktrees\",\"de\":\"Änderungsanfragen und Arbeitsbäume\",\"es\":\"Solicitudes de cambio y árboles de trabajo\",\"fr\":\"Demandes de modification et arbres de travail\",\"ja\":\"変更リクエストとワークツリー\",\"pt\":\"Solicitações de alteração e árvores de trabalho\",\"ru\":\"Запросы на изменение и рабочие деревья\",\"zh\":\"变更请求与工作树\"},\"description\":{\"en\":\"Track each Session's change requests with their checks and reviews, and start new Sessions in a Git worktree; uses the gh CLI for GitHub.\",\"de\":\"Verfolgen Sie die Änderungsanfragen jeder Sitzung mit ihren Prüfungen und Reviews und starten Sie neue Sitzungen in einem Git-Arbeitsbaum; nutzt die gh CLI für GitHub.\",\"es\":\"Siga las solicitudes de cambio de cada sesión con sus comprobaciones y revisiones, e inicie nuevas sesiones en un árbol de trabajo de Git; usa la CLI gh para GitHub.\",\"fr\":\"Suivez les demandes de modification de chaque session avec leurs vérifications et revues, et démarrez de nouvelles sessions dans un arbre de travail Git ; utilise la CLI gh pour GitHub.\",\"ja\":\"各セッションの変更リクエストとそのチェックやレビューを追跡し、新しいセッションを Git ワークツリーで開始できます。GitHub には gh CLI を使用します。\",\"pt\":\"Acompanhe as solicitações de alteração de cada sessão com suas verificações e revisões e inicie novas sessões em uma árvore de trabalho Git; usa a CLI gh para o GitHub.\",\"ru\":\"Отслеживайте запросы на изменение каждого сеанса вместе с их проверками и ревью и начинайте новые сеансы в рабочем дереве Git; для GitHub используется gh CLI.\",\"zh\":\"跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-subagent-claude-code\",\"title\":{\"de\":\"Claude Code-Subagent\",\"en\":\"Claude Code subagent\",\"es\":\"Subagente Claude Code\",\"fr\":\"Sous-agent Claude Code\",\"ja\":\"Claude Code サブエージェント\",\"pt\":\"Claude Code subagente\",\"ru\":\"Субагент Claude Code\",\"zh\":\"Claude Code 子智能体\"},\"description\":{\"de\":\"Delegieren Sie eine Aufgabe an Claude Code, indem Sie dessen native Authentifizierung und Einstellungen verwenden.\",\"en\":\"Delegate one task to Claude Code using its native authentication and settings.\",\"es\":\"Delegue una tarea a Claude Code utilizando su autenticación y configuración nativas.\",\"fr\":\"Déléguez une tâche à Claude Code à l’aide de son authentification et de ses paramètres natifs.\",\"ja\":\"Claude Code のネイティブの認証と設定を使用して、単独のタスクを委派します。\",\"pt\":\"Delegue uma tarefa a Claude Code usando sua autenticação e configurações nativas.\",\"ru\":\"Делегируйте одну задачу Claude Code, используя его встроенную аутентификацию и настройки.\",\"zh\":\"使用 Claude Code 原生认证和设置委派单个任务。\"},\"enabled\":false},{\"name\":\"@deepseek-ai/dsh-subagent-codex\",\"title\":{\"de\":\"Codex-Subagent\",\"en\":\"Codex subagent\",\"es\":\"Subagente Codex\",\"fr\":\"Sous-agent Codex\",\"ja\":\"Codex サブエージェント\",\"pt\":\"Codex subagente\",\"ru\":\"Субагент Codex\",\"zh\":\"Codex 子智能体\"},\"description\":{\"de\":\"Delegieren Sie eine Aufgabe an Codex, indem Sie dessen native Authentifizierung und Einstellungen verwenden.\",\"en\":\"Delegate one task to Codex using its native authentication and settings.\",\"es\":\"Delegue una tarea a Codex utilizando su autenticación y configuración nativas.\",\"fr\":\"Déléguez une tâche à Codex à l’aide de son authentification et de ses paramètres natifs.\",\"ja\":\"Codex のネイティブの認証と設定を使用して、単独のタスクを委派します。\",\"pt\":\"Delegue uma tarefa a Codex usando sua autenticação e configurações nativas.\",\"ru\":\"Делегируйте одну задачу Codex, используя его встроенную аутентификацию и настройки.\",\"zh\":\"使用 Codex 原生认证和设置委派单个任务。\"},\"enabled\":false}]\n\n"}],"isError":false,"id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["apply_patch","ask_user_question","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","plugin_manager","present","propose_action","read","read_image","schedule_create","schedule_edit","schedule_list","schedule_view","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}