From 1ee06e30155a244c2615406604bd4ca7b580e9e1 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 16 Jun 2026 23:49:14 +0800 Subject: [PATCH 01/10] docs(cua): plan target support --- .../cua-cross-platform-computer-use/plan.md | 172 +++++++++++++ .../cua-cross-platform-computer-use/spec.md | 229 ++++++++++++++++++ .../cua-cross-platform-computer-use/tasks.md | 107 ++++++++ 3 files changed, 508 insertions(+) create mode 100644 docs/features/cua-cross-platform-computer-use/plan.md create mode 100644 docs/features/cua-cross-platform-computer-use/spec.md create mode 100644 docs/features/cua-cross-platform-computer-use/tasks.md diff --git a/docs/features/cua-cross-platform-computer-use/plan.md b/docs/features/cua-cross-platform-computer-use/plan.md new file mode 100644 index 000000000..7fe8a62ad --- /dev/null +++ b/docs/features/cua-cross-platform-computer-use/plan.md @@ -0,0 +1,172 @@ +# CUA Cross-Platform Computer Use Plan + +## Design Principles + +- Keep the DeepChat integration model unchanged: official plugin, skill, and DeepChat-owned tool + startup. +- Treat upstream CUA release artifacts as immutable inputs pinned by tag, commit, asset name, and + checksum. +- Fail closed when a target runtime is unavailable or an archive layout does not match expectations. +- Avoid runtime network activity. All downloads happen at build time. +- Keep packaging verification close to the produced `.dcplugin` files, not only source folders. + +## Current-State Changes Required + +### Plugin Manifest + +Update `plugins/cua/plugin.json`: + +- Change platform support from macOS-only to target-aware support for `darwin/arm64`, + `darwin/x64`, `win32/x64`, and `linux/x64`. +- Add or enforce arch-aware visibility metadata so `win32/arm64` and `linux/arm64` do not show CUA + as an available official plugin. +- Replace macOS-only runtime candidates with platform-specific candidates: + - `plugin:runtime/darwin/${arch}/CuaDriver.app/Contents/MacOS/cua-driver` + - `plugin:runtime/win32/${arch}/cua-driver.exe` + - `plugin:runtime/linux/${arch}/cua-driver` +- Keep plugin-local runtime candidates first. +- Update packaged download URL conventions to include target platform and arch. +- Update tool policies for upstream v0.5.5 tools. +- Keep the internal tool server declaration owned by the plugin host; do not add user-facing MCP + setup instructions. + +### Upstream Metadata + +Update `plugins/cua/vendor/cua-driver/upstream.json` from the old Swift fork metadata to the pinned +Rust driver release: + +- `source`: upstream `trycua/cua`. +- `tag`: `cua-driver-rs-v0.5.5`. +- `commit`: `d6dea4bc3c3a65ce821261752067cae8200fe5d6`. +- `version`: `0.5.5`. +- Include the expected asset map and checksums source. +- Record Windows arm64 and Linux arm64 as unsupported for this pinned DeepChat integration. + +### Runtime Staging + +Replace the macOS-only Swift build path in `scripts/build-cua-plugin-runtime.mjs` with a staging +pipeline: + +1. Resolve target platform and arch from CLI flags or host defaults. +2. Map supported DeepChat platform/arch targets to upstream asset names. +3. Download the upstream release archive and `checksums.txt` into a cache directory. +4. Verify the archive digest. +5. Extract into a temporary staging directory. +6. Validate the extracted layout. +7. Copy the normalized runtime files into `plugins/cua/runtime//`. +8. Set executable permissions for macOS and Linux. +9. Run host-executable smoke checks where possible. +10. Run macOS app bundle and signing checks for darwin targets. + +The script should reject Windows arm64, Linux arm64, and any other unsupported target with a clear +message before any partial runtime is staged. + +### Plugin Packaging + +Update `scripts/package-plugin.mjs`: + +- Remove the darwin-only CUA guard. +- Keep only the selected `runtime//` subtree in the `.dcplugin` artifact. +- Validate required files per target: + - macOS: helper app executable. + - Windows: `cua-driver.exe` and `cua-driver-uia.exe`. + - Linux: `cua-driver`. +- Preserve executable bits on POSIX archive entries. +- Keep source manifest hydration deterministic for platform and arch. + +### Build Scripts + +Update `package.json` scripts so CUA can be staged, bundled, and verified on supported platforms: + +- Add Windows x64 CUA build scripts. +- Add Linux x64 CUA build script. +- Keep Windows arm64 and Linux arm64 unsupported unless they are explicitly validated for DeepChat. +- Avoid duplicate runtime staging when `plugin:bundle` already invokes a build script. Either make + the build script idempotent and cheap when the target runtime is current, or split staging from + bundling explicitly. +- Ensure supported Windows and Linux build scripts include the CUA bundle step without affecting + unsupported arm builds. + +### CI and Release Workflows + +Update `.github/workflows/build.yml` and `.github/workflows/release.yml`: + +- Bundle and verify CUA on macOS arm64/x64. +- Bundle and verify CUA on Windows x64. +- Bundle and verify CUA on Linux x64. +- Do not request Windows arm64 or Linux arm64 CUA artifacts until those targets are explicitly + supported. +- Keep CUA verification next to Feishu verification so missing official plugin artifacts fail the + build. + +### Skill Docs + +Adapt CUA skill docs from upstream v0.5.5 into DeepChat-specific docs: + +- Remove upstream manual install, PATH, and standalone MCP setup requirements. +- Describe the DeepChat tool surface and platform behavior. +- Add platform caveats for macOS permissions, Windows foreground/background dispatch, and Linux + pre-release limitations. +- Replace Swift-era tool names with v0.5.5 tool names. +- Keep plugin support metadata aligned with the supported platform/arch matrix. + +### Settings and Permission UX + +Update plugin settings/runtime status code where needed: + +- Keep macOS helper-app permission checks. +- Show platform-neutral runtime status for Windows and Linux. +- Avoid macOS-only permission copy on non-macOS platforms. +- Ensure missing Windows arm64 and Linux arm64 runtimes are reported as unsupported, not as broken + installs. + +### Tests + +Update and add focused tests for: + +- Official plugin target metadata, visibility, and runtime candidate resolution. +- CUA manifest hydration and visibility for supported platform/arch targets. +- Runtime packaging validation per platform and arch. +- Unsupported Windows arm64 and Linux arm64 behavior. +- Tool policy coverage for upstream v0.5.5 known tools. +- Skill docs no longer asserting macOS-only or user-managed MCP-only language. +- Build and release workflow assertions for CUA on Windows, macOS, and Linux x64. + +## Verification Plan + +Run these after implementation: + +```bash +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck +pnpm test -- test/main/presenter/pluginPresenter.test.ts +pnpm test -- test/main/scripts +``` + +Run packaging checks on supported host/CI targets: + +```bash +pnpm run plugin:bundle -- --name cua --platform win32 --arch x64 +pnpm run plugin:verify -- --name cua --platform win32 --arch x64 +pnpm run plugin:bundle -- --name cua --platform linux --arch x64 +pnpm run plugin:verify -- --name cua --platform linux --arch x64 +pnpm run plugin:bundle -- --name cua --platform darwin --arch arm64 +pnpm run plugin:verify -- --name cua --platform darwin --arch arm64 +pnpm run plugin:bundle -- --name cua --platform darwin --arch x64 +pnpm run plugin:verify -- --name cua --platform darwin --arch x64 +``` + +On Windows, also verify the built `.dcplugin` contains both `cua-driver.exe` and +`cua-driver-uia.exe`. On Linux, verify `cua-driver` is executable after extraction. On macOS, +verify the helper app executable path and signing state. + +## Rollout Notes + +- This change should land as one focused feature branch because manifest, packaging, docs, and CI + must stay in sync. +- If upstream publishes a newer driver before implementation starts, re-run the release asset audit + and update the pinned tag only after confirming asset names, tool names, and Linux availability. +- If macOS helper-app signing fails after staging the upstream bundle, keep the runtime update but + isolate the signing fix in the staging script instead of changing the plugin host. diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md new file mode 100644 index 000000000..67ad88e51 --- /dev/null +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -0,0 +1,229 @@ +# CUA Cross-Platform Computer Use Spec + +## Status + +Draft for implementation planning. + +## Background + +DeepChat currently ships the CUA computer-use capability as an official plugin under +`plugins/cua`. The integration is DeepChat-managed: the plugin declares a skill and a bundled +tool server that DeepChat starts internally. Users do not configure an external MCP server, install +the CUA driver manually, or rely on PATH for the bundled experience. + +The current plugin is macOS-only: + +- `plugins/cua/plugin.json` limits `engines.platforms` to `darwin`. +- The runtime build script builds the older Swift driver from the vendored CUA fork. +- The package script special-cases only `runtime/darwin/`. +- Build and release workflows only include the CUA plugin in macOS artifacts. +- Skill docs, runtime permission wording, tests, and packaging docs assume macOS. + +Upstream `trycua/cua` now publishes the Rust CUA driver as cross-platform release artifacts. The +latest verified driver release for this plan is `cua-driver-rs-v0.5.5`, published on +2026-06-16. DeepChat support for this feature is limited to the targets that have been validated +for bundled plugin packaging: + +- macOS arm64 and x86_64, plus universal variants. +- Windows x86_64. +- Linux x86_64. + +Windows arm64 and Linux arm64 remain unsupported for this DeepChat integration until they are +explicitly validated. Upstream documents Linux support as pre-release. DeepChat should expose Linux +support where the runtime asset exists, while keeping Linux limitations explicit in docs and +validation. + +## Goal + +Update the official DeepChat CUA plugin from the older macOS-only driver integration to the latest +cross-platform upstream CUA driver release, so packaged DeepChat builds can use computer-use tools +on macOS, Windows, and Linux without requiring user-managed MCP setup or manual CUA installation. + +## Non-Goals + +- Do not switch DeepChat to user-managed MCP configuration for CUA. +- Do not require PATH-installed `cua-driver` for the bundled plugin. +- Do not run upstream install or uninstall scripts at app runtime. +- Do not introduce auto-start services, scheduled tasks, or package-manager installation from + inside DeepChat. +- Do not claim Windows arm64 or Linux arm64 CUA support until those targets are explicitly + validated for DeepChat packaging. +- Do not redesign the plugin host or the global tool permission model. + +## Platform Scope + +The implementation must support these packaged plugin targets: + +| DeepChat platform | DeepChat arch | Upstream asset status | Required behavior | +| --- | --- | --- | --- | +| `darwin` | `arm64` | Available | Bundle and verify CUA runtime | +| `darwin` | `x64` | Available | Bundle and verify CUA runtime | +| `win32` | `x64` | Available | Bundle and verify CUA runtime | +| `win32` | `arm64` | Unsupported for DeepChat | Do not bundle or show CUA; fail clearly if requested directly | +| `linux` | `x64` | Available | Bundle and verify CUA runtime | +| `linux` | `arm64` | Unsupported for DeepChat | Do not bundle or show CUA; fail clearly if requested directly | + +## Visibility Scope + +CUA support is target-based, not only platform-based. The plugin must be visible only for these +runtime targets: + +- `darwin/arm64` +- `darwin/x64` +- `win32/x64` +- `linux/x64` + +The plugin must not be visible as an official usable plugin on: + +- `win32/arm64` +- `linux/arm64` + +If the current plugin manifest can only express platform support, implementation must add an +arch-aware gate through manifest metadata, official-plugin discovery, or runtime support checks. +`engines.platforms` alone is not sufficient for CUA because Windows arm64 and Linux arm64 must stay +hidden even though their operating systems are otherwise in scope. + +## Integration Contract + +DeepChat must continue to own the integration boundary: + +- The official plugin manifest or discovery layer declares the supported platform/arch targets and + bundled runtime candidates. +- The driver binary is started by DeepChat's plugin host through the existing plugin tool server + path. +- The user-facing capability remains "skill + built-in tool surface" inside DeepChat. +- The implementation may keep the internal stdio server transport, but it must not require users + to configure or install an external MCP server. +- Runtime detection must prefer plugin-local binaries and only use external fallback candidates for + diagnostics or development. + +## Upstream Runtime Contract + +Pin the CUA runtime to a specific upstream release: + +- Tag: `cua-driver-rs-v0.5.5`. +- Commit: `d6dea4bc3c3a65ce821261752067cae8200fe5d6`. +- Version: `0.5.5`. + +The build step must stage release artifacts instead of relying on local Swift-only source builds. +Every staged asset must be validated before packaging: + +- Download the expected release archive for the target platform and arch. +- Verify it against the upstream `checksums.txt` asset. +- Validate required files exist after extraction. +- Normalize executable permissions on POSIX targets. +- Validate the driver can be executed for a low-risk command such as `--version` when the host + platform can run the target binary. +- Keep macOS signing and helper-app validation in place where a `.app` bundle is staged. + +## Runtime Layout + +The packaged plugin should stage only the target runtime needed by the artifact being built: + +```text +plugins/cua/runtime/ + darwin/ + arm64/ + CuaDriver.app/Contents/MacOS/cua-driver + x64/ + CuaDriver.app/Contents/MacOS/cua-driver + win32/ + x64/ + cua-driver.exe + cua-driver-uia.exe + linux/ + x64/ + cua-driver +``` + +If implementation inspection shows that DeepChat must keep the previous helper app display name, +the macOS app directory may remain `DeepChat Computer Use.app`, but the staged bundle must still +preserve a valid Info.plist, executable path, and code signature after any rename or re-sign step. + +## Tool Surface + +The plugin policy and skill docs must match upstream v0.5.5 tool names. + +Removed or renamed assumptions: + +- Do not expose `screenshot` as the primary capture tool. Upstream uses `get_window_state` with a + vision capture mode. +- Do not rely on `set_recording`. Recording is split into `start_recording`, + `stop_recording`, `get_recording_state`, `replay_trajectory`, and `install_ffmpeg`. + +Core tools expected across supported platforms include: + +- App and window discovery: `list_apps`, `list_windows`, `get_window_state`, + `get_accessibility_tree`. +- App and window actions: `launch_app`, `kill_app`, `bring_to_front`. +- Input actions: `click`, `double_click`, `right_click`, `drag`, `scroll`, `type_text`, + `press_key`, `hotkey`, `set_value`. +- Cursor tools: `get_screen_size`, `get_cursor_position`, `move_cursor`, + `set_agent_cursor_enabled`, `set_agent_cursor_motion`, `set_agent_cursor_style`, + `get_agent_cursor_state`. +- Configuration and permissions: `check_permissions`, `get_config`, `set_config`, + `check_for_update`. +- Session and recording lifecycle: `start_session`, `end_session`, `start_recording`, + `stop_recording`, `get_recording_state`, `replay_trajectory`, `install_ffmpeg`. + +Platform-specific tools may exist, such as Linux mouse-button primitives and Windows diagnostic +tools. Policies must classify these explicitly instead of leaving them to default approval rules. + +## Permission and Safety Requirements + +Tool policies must be exact and conservative: + +- Read-only discovery and status tools may be allowed automatically. +- User-visible input, app launch, app termination, window focus, recording, replay, config + mutation, and dependency installation must require user approval. +- Any newly detected upstream tool without a policy must be treated as a review failure in tests. + +Platform permission behavior must be explicit: + +- macOS keeps accessibility and screen-capture permission checks and helper-app permission UX. +- Windows must not show macOS TCC-specific instructions. +- Linux must communicate pre-release constraints and compositor/session limitations without + blocking supported tool startup when the driver reports usable status. + +## Packaging Requirements + +The packaged app must keep CUA usable after Electron packaging: + +- The `.dcplugin` artifact must contain the correct runtime subtree for its platform and arch. +- Runtime files must stay outside `app.asar`. +- Supported Windows archives must include `cua-driver-uia.exe` next to `cua-driver.exe`. +- Linux runtime files must retain executable permissions after package extraction. +- macOS helper bundles must pass bundle path, executable, and signing validation. +- `plugin:verify` must be able to verify CUA artifacts per supported platform and arch. +- CI and release workflows must bundle and verify CUA for supported Windows, macOS, and Linux + build targets. + +## Acceptance Criteria + +- Official CUA plugin metadata or discovery logic allows only the supported target matrix: + `darwin/arm64`, `darwin/x64`, `win32/x64`, and `linux/x64`. +- Packaged macOS, Windows, and Linux x64 builds include a CUA `.dcplugin` artifact. +- Packaged Windows arm64 and Linux arm64 builds do not include a visible or usable CUA plugin. +- Direct CUA runtime packaging for Windows arm64 or Linux arm64 fails with a clear unsupported-target + message. +- Official plugin visibility is gated by platform and arch, so unsupported arm targets do not show + CUA as available. +- Runtime detection resolves the plugin-local binary on every supported target. +- The plugin starts through DeepChat's internal tool path without user-managed MCP setup. +- Skill docs describe DeepChat usage and platform caveats, not upstream manual installer workflows. +- Tool policies cover all upstream v0.5.5 tools known to this integration. +- Packaging docs and tests no longer describe CUA as macOS-only. +- Build, lint, i18n, and focused test suites pass after implementation. + +## Risks + +- Upstream release archive layouts may change. The staging script must validate layout and fail + closed. +- Cross-compiling the Rust driver locally is higher risk than consuming verified release assets. + The first implementation should prefer release assets. +- macOS helper-app rename or re-signing can break permissions. The implementation must verify the + staged bundle after any mutation. +- Linux support is upstream pre-release. DeepChat should support the available asset while keeping + limitations visible and testable. +- Tool names changed from the Swift-era integration. Missing policy updates could silently approve + or block the wrong tools. diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md new file mode 100644 index 000000000..31cf1536f --- /dev/null +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -0,0 +1,107 @@ +# CUA Cross-Platform Computer Use Tasks + +## Task List + +- [ ] T01 - Update CUA upstream metadata + - Replace old Swift fork metadata with pinned `cua-driver-rs-v0.5.5` metadata. + - Record supported and unsupported platform/arch targets. + - Add expected upstream asset names and checksum source. + +- [ ] T02 - Rewrite CUA runtime staging + - Replace macOS-only Swift build logic in `scripts/build-cua-plugin-runtime.mjs`. + - Add release asset download, checksum verification, extraction, layout validation, and runtime + copy. + - Add target mapping for darwin arm64/x64, win32 x64, and linux x64. + - Fail clearly for win32 arm64 and linux arm64. + +- [ ] T03 - Validate staged runtime files + - Validate macOS helper app executable path and signing state. + - Validate Windows `cua-driver.exe` plus `cua-driver-uia.exe`. + - Validate Linux `cua-driver` and executable permissions. + - Add host-compatible `--version` smoke checks. + +- [ ] T04 - Update CUA plugin manifest + - Expand support from macOS-only to the supported target matrix. + - Add or enforce arch-aware plugin visibility. + - Add platform-specific plugin-local runtime detect candidates. + - Keep CUA hidden on win32 arm64 and linux arm64. + - Update source URL pattern for platform and arch artifacts. + - Keep the DeepChat-owned internal tool server startup path. + +- [ ] T05 - Update CUA tool policies + - Remove Swift-era `screenshot` and `set_recording` assumptions. + - Add policies for v0.5.5 read-only, action, recording, session, update, and platform-specific + tools. + - Add a test that fails when a known upstream tool lacks an explicit policy. + +- [ ] T06 - Update plugin packaging + - Remove darwin-only CUA validation in `scripts/package-plugin.mjs`. + - Package only the selected `runtime//` subtree. + - Preserve POSIX executable permissions. + - Verify the `.dcplugin` artifact contains the expected files for each supported target. + +- [ ] T07 - Update package scripts + - Add CUA build/bundle support for Windows x64 and Linux x64. + - Include CUA in supported Windows and Linux app build scripts. + - Keep Windows arm64 and Linux arm64 from bundling an unusable CUA plugin. + - Avoid unnecessary duplicate staging during bundle commands. + +- [ ] T08 - Update CI and release workflows + - Bundle and verify CUA in macOS, Windows, and Linux x64 build jobs. + - Skip CUA for Windows arm64 and Linux arm64 jobs. + - Skip CUA only where the target is intentionally unsupported. + - Keep official plugin verification failing on missing expected artifacts. + +- [ ] T09 - Update DeepChat skill docs + - Adapt upstream v0.5.5 skill guidance to DeepChat's bundled integration. + - Remove manual installer, PATH, and user-managed MCP setup language. + - Add macOS, Windows, and Linux platform caveats. + - Replace old tool names with v0.5.5 tool names. + +- [ ] T10 - Update settings and permission status + - Keep macOS accessibility and screen-capture permission handling. + - Make Windows and Linux runtime status platform-aware. + - Show unsupported runtime status for win32 arm64 and linux arm64 rather than broken-install + language. + - Avoid macOS-only instructions on non-macOS platforms. + +- [ ] T11 - Update tests + - Update `test/main/presenter/pluginPresenter.test.ts` for cross-platform manifest behavior, + skill docs, metadata, and workflow expectations. + - Add or update package script tests for CUA target validation. + - Keep macOS signing tests focused on macOS helper behavior. + - Add negative tests for unsupported win32 arm64 and linux arm64 packaging and visibility. + +- [ ] T12 - Update packaging documentation + - Update `docs/guides/plugin-packaging.md` so CUA is no longer described as macOS-only. + - Document platform/arch artifact expectations. + - Document the no-runtime-installer and plugin-local-runtime requirement. + +- [ ] T13 - Run local verification + - Run formatting, i18n, lint, typecheck, and focused tests. + - Bundle and verify the Windows x64 CUA plugin on the current Windows host. + - Inspect the generated `.dcplugin` archive contents. + +- [ ] T14 - Verify CI-only targets + - Use CI to validate macOS arm64/x64 packaging and signing. + - Use CI to validate Linux x64 packaging and executable permissions. + - Use CI to validate Windows x64 packaging. + - Confirm Windows arm64 and Linux arm64 jobs do not ship or show CUA. + +## Implementation Order + +1. T01, T02, and T03 establish the runtime input and safety checks. +2. T04, T05, and T09 align the plugin contract with the new runtime. +3. T06 and T07 make local packaging produce correct artifacts. +4. T08 and T12 keep release infrastructure and documentation aligned. +5. T10 and T11 close platform UX and regression coverage. +6. T13 and T14 verify the final artifacts. + +## Done Definition + +- CUA `.dcplugin` artifacts are produced and verified for every supported target. +- Packaged DeepChat builds include CUA where the upstream runtime exists. +- DeepChat users can access computer-use capability through the built-in skill/tool path without + manual CUA setup. +- Unsupported targets fail clearly during packaging and do not ship broken plugins. +- Tests and docs reflect cross-platform support and current upstream tool names. From ede49a39d85a8aa2e4e1d918522e864222652ba9 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 00:20:44 +0800 Subject: [PATCH 02/10] feat(cua): add cross-platform runtimes --- .github/workflows/build.yml | 5 +- .github/workflows/release.yml | 5 +- .../cua-cross-platform-computer-use/plan.md | 33 +- .../cua-cross-platform-computer-use/spec.md | 41 +- .../cua-cross-platform-computer-use/tasks.md | 50 +- docs/guides/plugin-packaging.md | 47 +- package.json | 23 +- plugins/cua/plugin.json | 28 +- plugins/cua/policies/tool-policy.json | 17 +- plugins/cua/settings/assets/index.js | 2 +- plugins/cua/skills/cua-driver/README.md | 21 +- plugins/cua/skills/cua-driver/RECORDING.md | 11 +- plugins/cua/skills/cua-driver/SKILL.md | 92 ++-- plugins/cua/skills/cua-driver/TESTS.md | 9 +- plugins/cua/skills/cua-driver/WEB_APPS.md | 9 +- plugins/cua/vendor/cua-driver/upstream.json | 40 +- scripts/build-cua-plugin-runtime.mjs | 433 +++++++++++------- scripts/package-plugin.mjs | 108 ++++- scripts/plugin.mjs | 14 +- src/main/presenter/pluginPresenter/index.ts | 28 +- src/shared/types/plugin.ts | 1 + test/main/presenter/pluginPresenter.test.ts | 317 ++++++------- 22 files changed, 826 insertions(+), 508 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47a4a0f73..495bb364f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,6 +79,7 @@ jobs: - name: Build Windows run: | pnpm run build + pnpm run plugin:bundle -- --name cua --platform win32 --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform win32 --arch ${{ matrix.arch }} pnpm exec electron-builder --win --${{ matrix.arch }} --publish=never env: @@ -90,6 +91,7 @@ jobs: - name: Verify bundled plugins shell: bash run: | + pnpm run plugin:verify -- --name cua --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/${{ matrix.unpacked }}/resources/app.asar.unpacked/plugins pnpm run plugin:verify -- --name feishu --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/${{ matrix.unpacked }}/resources/app.asar.unpacked/plugins - name: Upload artifacts @@ -142,6 +144,7 @@ jobs: - name: Build Linux run: | pnpm run build + pnpm run plugin:bundle -- --name cua --platform linux --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform linux --arch ${{ matrix.arch }} pnpm exec electron-builder --linux --${{ matrix.arch }} --publish=never env: @@ -153,6 +156,7 @@ jobs: - name: Verify bundled plugins shell: bash run: | + pnpm run plugin:verify -- --name cua --platform linux --arch ${{ matrix.arch }} --plugin-root dist/linux-unpacked/resources/app.asar.unpacked/plugins pnpm run plugin:verify -- --name feishu --platform linux --arch ${{ matrix.arch }} --plugin-root dist/linux-unpacked/resources/app.asar.unpacked/plugins - name: Upload artifacts @@ -213,7 +217,6 @@ jobs: - name: Build Mac run: | pnpm run build - pnpm run plugin:cua:build:mac:${{ matrix.arch }} pnpm run plugin:bundle -- --name cua --platform darwin --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform darwin --arch ${{ matrix.arch }} pnpm exec electron-builder --mac --${{ matrix.arch }} --publish=never diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70c17b089..d93ea4ec4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -173,6 +173,7 @@ jobs: - name: Build Windows run: | pnpm run build + pnpm run plugin:bundle -- --name cua --platform win32 --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform win32 --arch ${{ matrix.arch }} pnpm exec electron-builder --win --${{ matrix.arch }} --publish=never env: @@ -185,6 +186,7 @@ jobs: - name: Verify bundled plugins shell: bash run: | + pnpm run plugin:verify -- --name cua --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/${{ matrix.unpacked }}/resources/app.asar.unpacked/plugins pnpm run plugin:verify -- --name feishu --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/${{ matrix.unpacked }}/resources/app.asar.unpacked/plugins - name: Upload artifacts @@ -236,6 +238,7 @@ jobs: - name: Build Linux run: | pnpm run build + pnpm run plugin:bundle -- --name cua --platform linux --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform linux --arch ${{ matrix.arch }} pnpm exec electron-builder --linux --${{ matrix.arch }} --publish=never env: @@ -248,6 +251,7 @@ jobs: - name: Verify bundled plugins shell: bash run: | + pnpm run plugin:verify -- --name cua --platform linux --arch ${{ matrix.arch }} --plugin-root dist/linux-unpacked/resources/app.asar.unpacked/plugins pnpm run plugin:verify -- --name feishu --platform linux --arch ${{ matrix.arch }} --plugin-root dist/linux-unpacked/resources/app.asar.unpacked/plugins - name: Upload artifacts @@ -310,7 +314,6 @@ jobs: - name: Build Mac run: | pnpm run build - pnpm run plugin:cua:build:mac:${{ matrix.arch }} pnpm run plugin:bundle -- --name cua --platform darwin --arch ${{ matrix.arch }} pnpm run plugin:bundle -- --name feishu --platform darwin --arch ${{ matrix.arch }} pnpm exec electron-builder --mac --${{ matrix.arch }} --publish=never diff --git a/docs/features/cua-cross-platform-computer-use/plan.md b/docs/features/cua-cross-platform-computer-use/plan.md index 7fe8a62ad..c1a6e8e05 100644 --- a/docs/features/cua-cross-platform-computer-use/plan.md +++ b/docs/features/cua-cross-platform-computer-use/plan.md @@ -17,9 +17,9 @@ Update `plugins/cua/plugin.json`: - Change platform support from macOS-only to target-aware support for `darwin/arm64`, - `darwin/x64`, `win32/x64`, and `linux/x64`. -- Add or enforce arch-aware visibility metadata so `win32/arm64` and `linux/arm64` do not show CUA - as an available official plugin. + `darwin/x64`, `win32/x64`, `win32/arm64`, and `linux/x64`. +- Add or enforce arch-aware visibility metadata so `linux/arm64` does not show CUA as an available + official plugin. - Replace macOS-only runtime candidates with platform-specific candidates: - `plugin:runtime/darwin/${arch}/CuaDriver.app/Contents/MacOS/cua-driver` - `plugin:runtime/win32/${arch}/cua-driver.exe` @@ -40,7 +40,8 @@ Rust driver release: - `commit`: `d6dea4bc3c3a65ce821261752067cae8200fe5d6`. - `version`: `0.5.5`. - Include the expected asset map and checksums source. -- Record Windows arm64 and Linux arm64 as unsupported for this pinned DeepChat integration. +- Record Windows arm64 as supported and Linux arm64 as unsupported for this pinned DeepChat + integration. ### Runtime Staging @@ -58,8 +59,8 @@ pipeline: 9. Run host-executable smoke checks where possible. 10. Run macOS app bundle and signing checks for darwin targets. -The script should reject Windows arm64, Linux arm64, and any other unsupported target with a clear -message before any partial runtime is staged. +The script should reject Linux arm64 and any other unsupported target with a clear message before +any partial runtime is staged. ### Plugin Packaging @@ -78,24 +79,23 @@ Update `scripts/package-plugin.mjs`: Update `package.json` scripts so CUA can be staged, bundled, and verified on supported platforms: -- Add Windows x64 CUA build scripts. +- Add Windows x64 and arm64 CUA build scripts. - Add Linux x64 CUA build script. -- Keep Windows arm64 and Linux arm64 unsupported unless they are explicitly validated for DeepChat. +- Keep Linux arm64 unsupported unless it is explicitly validated for DeepChat. - Avoid duplicate runtime staging when `plugin:bundle` already invokes a build script. Either make the build script idempotent and cheap when the target runtime is current, or split staging from bundling explicitly. - Ensure supported Windows and Linux build scripts include the CUA bundle step without affecting - unsupported arm builds. + unsupported Linux arm64 builds. ### CI and Release Workflows Update `.github/workflows/build.yml` and `.github/workflows/release.yml`: - Bundle and verify CUA on macOS arm64/x64. -- Bundle and verify CUA on Windows x64. +- Bundle and verify CUA on Windows x64 and arm64. - Bundle and verify CUA on Linux x64. -- Do not request Windows arm64 or Linux arm64 CUA artifacts until those targets are explicitly - supported. +- Do not request Linux arm64 CUA artifacts until that target is explicitly supported. - Keep CUA verification next to Feishu verification so missing official plugin artifacts fail the build. @@ -117,8 +117,7 @@ Update plugin settings/runtime status code where needed: - Keep macOS helper-app permission checks. - Show platform-neutral runtime status for Windows and Linux. - Avoid macOS-only permission copy on non-macOS platforms. -- Ensure missing Windows arm64 and Linux arm64 runtimes are reported as unsupported, not as broken - installs. +- Ensure missing Linux arm64 runtimes are reported as unsupported, not as broken installs. ### Tests @@ -127,10 +126,10 @@ Update and add focused tests for: - Official plugin target metadata, visibility, and runtime candidate resolution. - CUA manifest hydration and visibility for supported platform/arch targets. - Runtime packaging validation per platform and arch. -- Unsupported Windows arm64 and Linux arm64 behavior. +- Unsupported Linux arm64 behavior. - Tool policy coverage for upstream v0.5.5 known tools. - Skill docs no longer asserting macOS-only or user-managed MCP-only language. -- Build and release workflow assertions for CUA on Windows, macOS, and Linux x64. +- Build and release workflow assertions for CUA on Windows x64/arm64, macOS, and Linux x64. ## Verification Plan @@ -150,6 +149,8 @@ Run packaging checks on supported host/CI targets: ```bash pnpm run plugin:bundle -- --name cua --platform win32 --arch x64 pnpm run plugin:verify -- --name cua --platform win32 --arch x64 +pnpm run plugin:bundle -- --name cua --platform win32 --arch arm64 +pnpm run plugin:verify -- --name cua --platform win32 --arch arm64 pnpm run plugin:bundle -- --name cua --platform linux --arch x64 pnpm run plugin:verify -- --name cua --platform linux --arch x64 pnpm run plugin:bundle -- --name cua --platform darwin --arch arm64 diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index 67ad88e51..3cff4c163 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -21,17 +21,17 @@ The current plugin is macOS-only: Upstream `trycua/cua` now publishes the Rust CUA driver as cross-platform release artifacts. The latest verified driver release for this plan is `cua-driver-rs-v0.5.5`, published on -2026-06-16. DeepChat support for this feature is limited to the targets that have been validated -for bundled plugin packaging: +2026-06-16. DeepChat support for this feature is limited to the targets that have upstream release +assets and have been validated for bundled plugin packaging: - macOS arm64 and x86_64, plus universal variants. -- Windows x86_64. +- Windows x86_64 and arm64. - Linux x86_64. -Windows arm64 and Linux arm64 remain unsupported for this DeepChat integration until they are -explicitly validated. Upstream documents Linux support as pre-release. DeepChat should expose Linux -support where the runtime asset exists, while keeping Linux limitations explicit in docs and -validation. +Linux arm64 remains unsupported for this DeepChat integration until upstream publishes and DeepChat +validates a matching release asset. Upstream documents Linux support as pre-release. DeepChat should +expose Linux support where the runtime asset exists, while keeping Linux limitations explicit in +docs and validation. ## Goal @@ -46,8 +46,8 @@ on macOS, Windows, and Linux without requiring user-managed MCP setup or manual - Do not run upstream install or uninstall scripts at app runtime. - Do not introduce auto-start services, scheduled tasks, or package-manager installation from inside DeepChat. -- Do not claim Windows arm64 or Linux arm64 CUA support until those targets are explicitly - validated for DeepChat packaging. +- Do not claim Linux arm64 CUA support until that target is explicitly validated for DeepChat + packaging. - Do not redesign the plugin host or the global tool permission model. ## Platform Scope @@ -59,7 +59,7 @@ The implementation must support these packaged plugin targets: | `darwin` | `arm64` | Available | Bundle and verify CUA runtime | | `darwin` | `x64` | Available | Bundle and verify CUA runtime | | `win32` | `x64` | Available | Bundle and verify CUA runtime | -| `win32` | `arm64` | Unsupported for DeepChat | Do not bundle or show CUA; fail clearly if requested directly | +| `win32` | `arm64` | Available | Bundle and verify CUA runtime | | `linux` | `x64` | Available | Bundle and verify CUA runtime | | `linux` | `arm64` | Unsupported for DeepChat | Do not bundle or show CUA; fail clearly if requested directly | @@ -71,17 +71,17 @@ runtime targets: - `darwin/arm64` - `darwin/x64` - `win32/x64` +- `win32/arm64` - `linux/x64` The plugin must not be visible as an official usable plugin on: -- `win32/arm64` - `linux/arm64` If the current plugin manifest can only express platform support, implementation must add an arch-aware gate through manifest metadata, official-plugin discovery, or runtime support checks. -`engines.platforms` alone is not sufficient for CUA because Windows arm64 and Linux arm64 must stay -hidden even though their operating systems are otherwise in scope. +`engines.platforms` alone is not sufficient for CUA because Linux arm64 must stay hidden even though +the operating system is otherwise in scope. ## Integration Contract @@ -131,6 +131,9 @@ plugins/cua/runtime/ x64/ cua-driver.exe cua-driver-uia.exe + arm64/ + cua-driver.exe + cua-driver-uia.exe linux/ x64/ cua-driver @@ -201,13 +204,13 @@ The packaged app must keep CUA usable after Electron packaging: ## Acceptance Criteria - Official CUA plugin metadata or discovery logic allows only the supported target matrix: - `darwin/arm64`, `darwin/x64`, `win32/x64`, and `linux/x64`. + `darwin/arm64`, `darwin/x64`, `win32/x64`, `win32/arm64`, and `linux/x64`. - Packaged macOS, Windows, and Linux x64 builds include a CUA `.dcplugin` artifact. -- Packaged Windows arm64 and Linux arm64 builds do not include a visible or usable CUA plugin. -- Direct CUA runtime packaging for Windows arm64 or Linux arm64 fails with a clear unsupported-target - message. -- Official plugin visibility is gated by platform and arch, so unsupported arm targets do not show - CUA as available. +- Packaged Windows arm64 builds include a CUA `.dcplugin` artifact. +- Packaged Linux arm64 builds do not include a visible or usable CUA plugin. +- Direct CUA runtime packaging for Linux arm64 fails with a clear unsupported-target message. +- Official plugin visibility is gated by platform and arch, so the unsupported Linux arm target does + not show CUA as available. - Runtime detection resolves the plugin-local binary on every supported target. - The plugin starts through DeepChat's internal tool path without user-managed MCP setup. - Skill docs describe DeepChat usage and platform caveats, not upstream manual installer workflows. diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md index 31cf1536f..c47d27632 100644 --- a/docs/features/cua-cross-platform-computer-use/tasks.md +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -2,91 +2,91 @@ ## Task List -- [ ] T01 - Update CUA upstream metadata +- [x] T01 - Update CUA upstream metadata - Replace old Swift fork metadata with pinned `cua-driver-rs-v0.5.5` metadata. - Record supported and unsupported platform/arch targets. - Add expected upstream asset names and checksum source. -- [ ] T02 - Rewrite CUA runtime staging +- [x] T02 - Rewrite CUA runtime staging - Replace macOS-only Swift build logic in `scripts/build-cua-plugin-runtime.mjs`. - Add release asset download, checksum verification, extraction, layout validation, and runtime copy. - - Add target mapping for darwin arm64/x64, win32 x64, and linux x64. - - Fail clearly for win32 arm64 and linux arm64. + - Add target mapping for darwin arm64/x64, win32 x64/arm64, and linux x64. + - Fail clearly for linux arm64. -- [ ] T03 - Validate staged runtime files +- [x] T03 - Validate staged runtime files - Validate macOS helper app executable path and signing state. - Validate Windows `cua-driver.exe` plus `cua-driver-uia.exe`. - Validate Linux `cua-driver` and executable permissions. - Add host-compatible `--version` smoke checks. -- [ ] T04 - Update CUA plugin manifest +- [x] T04 - Update CUA plugin manifest - Expand support from macOS-only to the supported target matrix. - Add or enforce arch-aware plugin visibility. - Add platform-specific plugin-local runtime detect candidates. - - Keep CUA hidden on win32 arm64 and linux arm64. + - Keep CUA hidden on linux arm64. - Update source URL pattern for platform and arch artifacts. - Keep the DeepChat-owned internal tool server startup path. -- [ ] T05 - Update CUA tool policies +- [x] T05 - Update CUA tool policies - Remove Swift-era `screenshot` and `set_recording` assumptions. - Add policies for v0.5.5 read-only, action, recording, session, update, and platform-specific tools. - Add a test that fails when a known upstream tool lacks an explicit policy. -- [ ] T06 - Update plugin packaging +- [x] T06 - Update plugin packaging - Remove darwin-only CUA validation in `scripts/package-plugin.mjs`. - Package only the selected `runtime//` subtree. - Preserve POSIX executable permissions. - Verify the `.dcplugin` artifact contains the expected files for each supported target. -- [ ] T07 - Update package scripts - - Add CUA build/bundle support for Windows x64 and Linux x64. +- [x] T07 - Update package scripts + - Add CUA build/bundle support for Windows x64/arm64 and Linux x64. - Include CUA in supported Windows and Linux app build scripts. - - Keep Windows arm64 and Linux arm64 from bundling an unusable CUA plugin. + - Keep Linux arm64 from bundling an unusable CUA plugin. - Avoid unnecessary duplicate staging during bundle commands. -- [ ] T08 - Update CI and release workflows - - Bundle and verify CUA in macOS, Windows, and Linux x64 build jobs. - - Skip CUA for Windows arm64 and Linux arm64 jobs. +- [x] T08 - Update CI and release workflows + - Bundle and verify CUA in macOS, Windows x64/arm64, and Linux x64 build jobs. + - Skip CUA for Linux arm64 jobs. - Skip CUA only where the target is intentionally unsupported. - Keep official plugin verification failing on missing expected artifacts. -- [ ] T09 - Update DeepChat skill docs +- [x] T09 - Update DeepChat skill docs - Adapt upstream v0.5.5 skill guidance to DeepChat's bundled integration. - Remove manual installer, PATH, and user-managed MCP setup language. - Add macOS, Windows, and Linux platform caveats. - Replace old tool names with v0.5.5 tool names. -- [ ] T10 - Update settings and permission status +- [x] T10 - Update settings and permission status - Keep macOS accessibility and screen-capture permission handling. - Make Windows and Linux runtime status platform-aware. - - Show unsupported runtime status for win32 arm64 and linux arm64 rather than broken-install - language. + - Show unsupported runtime status for linux arm64 rather than broken-install language. - Avoid macOS-only instructions on non-macOS platforms. -- [ ] T11 - Update tests +- [x] T11 - Update tests - Update `test/main/presenter/pluginPresenter.test.ts` for cross-platform manifest behavior, skill docs, metadata, and workflow expectations. - Add or update package script tests for CUA target validation. - Keep macOS signing tests focused on macOS helper behavior. - - Add negative tests for unsupported win32 arm64 and linux arm64 packaging and visibility. + - Add negative tests for unsupported linux arm64 packaging and visibility. -- [ ] T12 - Update packaging documentation +- [x] T12 - Update packaging documentation - Update `docs/guides/plugin-packaging.md` so CUA is no longer described as macOS-only. - Document platform/arch artifact expectations. - Document the no-runtime-installer and plugin-local-runtime requirement. -- [ ] T13 - Run local verification +- [x] T13 - Run local verification - Run formatting, i18n, lint, typecheck, and focused tests. - Bundle and verify the Windows x64 CUA plugin on the current Windows host. + - Bundle and verify the Windows arm64 CUA plugin without running the non-host binary. - Inspect the generated `.dcplugin` archive contents. - [ ] T14 - Verify CI-only targets - Use CI to validate macOS arm64/x64 packaging and signing. - Use CI to validate Linux x64 packaging and executable permissions. - - Use CI to validate Windows x64 packaging. - - Confirm Windows arm64 and Linux arm64 jobs do not ship or show CUA. + - Use CI to validate Windows x64/arm64 packaging. + - Confirm Linux arm64 jobs do not ship or show CUA. ## Implementation Order diff --git a/docs/guides/plugin-packaging.md b/docs/guides/plugin-packaging.md index 6dfaf8a27..6f808942c 100644 --- a/docs/guides/plugin-packaging.md +++ b/docs/guides/plugin-packaging.md @@ -80,31 +80,52 @@ step before packaging. The `bundle` action automatically detects and runs `scripts/build--plugin-runtime.mjs` when it exists. Standalone `package` expects the native runtime payload to be built already. -CUA native build commands (macOS-only, requires Swift toolchain): +CUA native runtime staging commands download pinned upstream release assets and verify their +checksums. They do not run upstream installers and do not require a PATH-installed `cua-driver`. ```bash -pnpm run plugin:cua:build # host architecture -pnpm run plugin:cua:build:mac:arm64 # explicit ARM64 -pnpm run plugin:cua:build:mac:x64 # explicit x64 +pnpm run plugin:cua:build # host platform and architecture +pnpm run plugin:cua:build:mac:arm64 # macOS arm64 +pnpm run plugin:cua:build:mac:x64 # macOS x64 +pnpm run plugin:cua:build:win:x64 # Windows x64 +pnpm run plugin:cua:build:win:arm64 # Windows arm64 +pnpm run plugin:cua:build:linux:x64 # Linux x64 ``` ## CUA Plugin Artifacts -The CUA plugin ships one macOS helper app per CPU architecture. The bundled package filename -includes both platform and architecture: +The CUA plugin is target-gated by platform and architecture. Supported bundled targets: + +- `darwin/arm64` +- `darwin/x64` +- `win32/x64` +- `win32/arm64` +- `linux/x64` + +Unsupported targets: + +- `linux/arm64` + +The bundled package filename includes both platform and architecture: ```text deepchat-plugin-cua--darwin-arm64.dcplugin deepchat-plugin-cua--darwin-x64.dcplugin +deepchat-plugin-cua--win32-x64.dcplugin +deepchat-plugin-cua--win32-arm64.dcplugin +deepchat-plugin-cua--linux-x64.dcplugin ``` Runtime detection inside the package uses architecture-specific paths: ```text -plugin:runtime/darwin//DeepChat Computer Use.app/Contents/MacOS/cua-driver +plugin:runtime/darwin//CuaDriver.app/Contents/MacOS/cua-driver +plugin:runtime/win32//cua-driver.exe +plugin:runtime/linux//cua-driver ``` -Each `.dcplugin` contains only the runtime directory for its target architecture. +Each `.dcplugin` contains only the runtime directory for its target platform and architecture. +Direct CUA packaging for unsupported targets fails before producing an artifact. ## Feishu Plugin Artifacts @@ -138,9 +159,10 @@ build/bundled-plugins/ The build matrix in `.github/workflows/build.yml` bundles plugins before running `electron-builder` on every platform: -- **macOS**: bundles both CUA (with native build) and feishu plugins. -- **Linux**: bundles feishu plugin only (CUA is macOS-only). -- **Windows**: bundles feishu plugin only. +- **macOS**: bundles both CUA and feishu plugins for arm64 and x64. +- **Linux x64**: bundles both CUA and feishu plugins. +- **Windows x64**: bundles both CUA and feishu plugins. +- **Windows arm64**: bundles both CUA and feishu plugins. Electron Builder embeds `.dcplugin` files from `build/bundled-plugins/` into: @@ -160,6 +182,9 @@ Expected embedded files (macOS example): ```text app.asar.unpacked/plugins/deepchat-plugin-cua--darwin-x64.dcplugin app.asar.unpacked/plugins/deepchat-plugin-cua--darwin-arm64.dcplugin +app.asar.unpacked/plugins/deepchat-plugin-cua--win32-x64.dcplugin +app.asar.unpacked/plugins/deepchat-plugin-cua--win32-arm64.dcplugin +app.asar.unpacked/plugins/deepchat-plugin-cua--linux-x64.dcplugin app.asar.unpacked/plugins/deepchat-plugin-feishu--darwin-x64.dcplugin app.asar.unpacked/plugins/deepchat-plugin-feishu--darwin-arm64.dcplugin ``` diff --git a/package.json b/package.json index afdc9fdbc..7651698f9 100644 --- a/package.json +++ b/package.json @@ -45,17 +45,20 @@ "plugin:verify": "node scripts/plugin.mjs verify", "plugin:bundle:clean": "node -e \"require('fs').rmSync('build/bundled-plugins',{recursive:true,force:true})\"", "plugin:cua:build": "node scripts/build-cua-plugin-runtime.mjs", - "plugin:cua:build:mac:arm64": "node scripts/build-cua-plugin-runtime.mjs --arch arm64", - "plugin:cua:build:mac:x64": "node scripts/build-cua-plugin-runtime.mjs --arch x64", + "plugin:cua:build:mac:arm64": "node scripts/build-cua-plugin-runtime.mjs --platform darwin --arch arm64", + "plugin:cua:build:mac:x64": "node scripts/build-cua-plugin-runtime.mjs --platform darwin --arch x64", + "plugin:cua:build:win:x64": "node scripts/build-cua-plugin-runtime.mjs --platform win32 --arch x64", + "plugin:cua:build:win:arm64": "node scripts/build-cua-plugin-runtime.mjs --platform win32 --arch arm64", + "plugin:cua:build:linux:x64": "node scripts/build-cua-plugin-runtime.mjs --platform linux --arch x64", "install:sharp": "node scripts/install-sharp-for-platform.js", - "build:mac": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:cua:build && pnpm run plugin:bundle -- --name cua --platform darwin && pnpm run plugin:bundle -- --name feishu --platform darwin && electron-builder --mac", - "build:mac:arm64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:cua:build:mac:arm64 && pnpm run plugin:bundle -- --name cua --platform darwin --arch arm64 && pnpm run plugin:bundle -- --name feishu --platform darwin --arch arm64 && electron-builder --mac --arm64", - "build:mac:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:cua:build:mac:x64 && pnpm run plugin:bundle -- --name cua --platform darwin --arch x64 && pnpm run plugin:bundle -- --name feishu --platform darwin --arch x64 && electron-builder --mac --x64", - "build:win": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform win32 && electron-builder --win", - "build:win:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform win32 --arch x64 && electron-builder --win --x64", - "build:win:arm64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform win32 --arch arm64 && electron-builder --win --arm64", - "build:linux": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform linux && electron-builder --linux", - "build:linux:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform linux --arch x64 && electron-builder --linux --x64", + "build:mac": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform darwin && pnpm run plugin:bundle -- --name feishu --platform darwin && electron-builder --mac", + "build:mac:arm64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform darwin --arch arm64 && pnpm run plugin:bundle -- --name feishu --platform darwin --arch arm64 && electron-builder --mac --arm64", + "build:mac:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform darwin --arch x64 && pnpm run plugin:bundle -- --name feishu --platform darwin --arch x64 && electron-builder --mac --x64", + "build:win": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform win32 && pnpm run plugin:bundle -- --name feishu --platform win32 && electron-builder --win", + "build:win:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform win32 --arch x64 && pnpm run plugin:bundle -- --name feishu --platform win32 --arch x64 && electron-builder --win --x64", + "build:win:arm64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform win32 --arch arm64 && pnpm run plugin:bundle -- --name feishu --platform win32 --arch arm64 && electron-builder --win --arm64", + "build:linux": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform linux && pnpm run plugin:bundle -- --name feishu --platform linux && electron-builder --linux", + "build:linux:x64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name cua --platform linux --arch x64 && pnpm run plugin:bundle -- --name feishu --platform linux --arch x64 && electron-builder --linux --x64", "build:linux:arm64": "pnpm run build && pnpm run plugin:bundle:clean && pnpm run plugin:bundle -- --name feishu --platform linux --arch arm64 && electron-builder --linux --arm64", "afterSign": "scripts/notarize.js", "installRuntime": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 && npx -y tiny-runtime-injector --type node --dir ./runtime/node && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk", diff --git a/plugins/cua/plugin.json b/plugins/cua/plugin.json index 1f63223fc..cd84fe753 100644 --- a/plugins/cua/plugin.json +++ b/plugins/cua/plugin.json @@ -5,7 +5,8 @@ "publisher": "DeepChat", "engines": { "deepchat": ">=${app.version}", - "platforms": ["darwin"] + "platforms": ["darwin", "win32", "linux"], + "targets": ["darwin/arm64", "darwin/x64", "win32/x64", "win32/arm64", "linux/x64"] }, "activationEvents": ["onEnable"], "capabilities": [ @@ -18,7 +19,7 @@ ], "source": { "type": "deepchat-official", - "url": "${github.release.download}/deepchat-plugin-cua-${app.version}-darwin-${arch}.dcplugin", + "url": "${github.release.download}/deepchat-plugin-cua-${app.version}-${target.platform}-${arch}.dcplugin", "publisher": "DeepChat" }, "runtime": { @@ -26,14 +27,16 @@ "type": "external-helper", "displayName": "Cua Driver", "detect": [ - "plugin:runtime/darwin/${arch}/DeepChat Computer Use.app/Contents/MacOS/cua-driver", + "plugin:runtime/darwin/${arch}/CuaDriver.app/Contents/MacOS/cua-driver", + "plugin:runtime/win32/${arch}/cua-driver.exe", + "plugin:runtime/linux/${arch}/cua-driver", "/Applications/CuaDriver.app/Contents/MacOS/cua-driver" ], "install": { "mode": "user-confirmed", "provider": "trycua", "strategy": "bundled-plugin-helper", - "minVersion": "0.1.0", + "minVersion": "0.5.5", "guideUrl": "https://cua.ai/docs/cua-driver/guide/getting-started/installation" } }, @@ -77,24 +80,37 @@ "list_windows": "allow", "get_screen_size": "allow", "get_window_state": "allow", + "get_accessibility_tree": "allow", "get_cursor_position": "allow", "get_config": "allow", "get_recording_state": "allow", "get_agent_cursor_state": "allow", - "screenshot": "allow", + "check_for_update": "allow", + "debug_window_info": "allow", + "start_session": "allow", + "end_session": "allow", "launch_app": "ask", + "kill_app": "ask", + "bring_to_front": "ask", "click": "ask", "right_click": "ask", "double_click": "ask", "drag": "ask", + "mouse_button_down": "ask", + "mouse_button_up": "ask", + "mouse_drag": "ask", + "parallel_mouse_drag": "ask", "scroll": "ask", "move_cursor": "ask", "type_text": "ask", + "type_text_chars": "ask", "press_key": "ask", "hotkey": "ask", "set_value": "ask", "set_config": "ask", - "set_recording": "ask", + "start_recording": "ask", + "stop_recording": "ask", + "install_ffmpeg": "ask", "set_agent_cursor_enabled": "ask", "set_agent_cursor_motion": "ask", "set_agent_cursor_style": "ask", diff --git a/plugins/cua/policies/tool-policy.json b/plugins/cua/policies/tool-policy.json index 54ab10168..167e1d685 100644 --- a/plugins/cua/policies/tool-policy.json +++ b/plugins/cua/policies/tool-policy.json @@ -6,24 +6,37 @@ "list_windows": "allow", "get_screen_size": "allow", "get_window_state": "allow", + "get_accessibility_tree": "allow", "get_cursor_position": "allow", "get_config": "allow", "get_recording_state": "allow", "get_agent_cursor_state": "allow", - "screenshot": "allow", + "check_for_update": "allow", + "debug_window_info": "allow", + "start_session": "allow", + "end_session": "allow", "launch_app": "ask", + "kill_app": "ask", + "bring_to_front": "ask", "click": "ask", "right_click": "ask", "double_click": "ask", "drag": "ask", + "mouse_button_down": "ask", + "mouse_button_up": "ask", + "mouse_drag": "ask", + "parallel_mouse_drag": "ask", "scroll": "ask", "move_cursor": "ask", "type_text": "ask", + "type_text_chars": "ask", "press_key": "ask", "hotkey": "ask", "set_value": "ask", "set_config": "ask", - "set_recording": "ask", + "start_recording": "ask", + "stop_recording": "ask", + "install_ffmpeg": "ask", "set_agent_cursor_enabled": "ask", "set_agent_cursor_motion": "ask", "set_agent_cursor_style": "ask", diff --git a/plugins/cua/settings/assets/index.js b/plugins/cua/settings/assets/index.js index ddcb89ac4..880abffe2 100644 --- a/plugins/cua/settings/assets/index.js +++ b/plugins/cua/settings/assets/index.js @@ -67,7 +67,7 @@ async function refreshStatus() { setText(runtimeStateNode, status.runtime?.state) setText(runtimeVersionNode, status.runtime?.version) setText(runtimeCommandNode, status.runtime?.command) - setText(runtimeHelperAppNode, status.runtime?.helperAppPath) + setText(runtimeHelperAppNode, status.runtime?.helperAppPath || 'Not required on this platform') const cuaMcp = status.mcpServers?.find((server) => server.serverId === 'cua-driver') if (!cuaMcp) { diff --git a/plugins/cua/skills/cua-driver/README.md b/plugins/cua/skills/cua-driver/README.md index 598acc612..dd7f9395b 100644 --- a/plugins/cua/skills/cua-driver/README.md +++ b/plugins/cua/skills/cua-driver/README.md @@ -1,6 +1,6 @@ -# CUA MCP Workflow +# CUA Computer Use Workflow -This skill uses DeepChat's plugin-provided `cua-driver` MCP tools. +This skill uses DeepChat's plugin-provided Computer Use tools. Core workflow: @@ -11,8 +11,19 @@ Core workflow: 5. UI action tool 6. `get_window_state` -Use element indices only after a snapshot for the same `pid` and `window_id`. Use pixel coordinates when the screenshot clearly shows a target missing from the accessibility tree. +Use element indices only after a snapshot for the same `pid` and `window_id`. Use pixel coordinates +when the vision capture clearly shows a target missing from the accessibility tree. -Permission setup uses `check_permissions`. The macOS grants belong to: +Supported bundled targets: -`${PLUGIN_ROOT}/runtime/darwin/${PROCESS_ARCH}/DeepChat Computer Use.app` +- `darwin/arm64` +- `darwin/x64` +- `win32/x64` +- `win32/arm64` +- `linux/x64` + +Unsupported bundled targets: + +- `linux/arm64` + +Do not ask the user to install CUA manually for DeepChat's bundled plugin. diff --git a/plugins/cua/skills/cua-driver/RECORDING.md b/plugins/cua/skills/cua-driver/RECORDING.md index 7436af9f8..64e056917 100644 --- a/plugins/cua/skills/cua-driver/RECORDING.md +++ b/plugins/cua/skills/cua-driver/RECORDING.md @@ -1,9 +1,14 @@ # Recording -Recording is controlled through MCP tools: +Recording is controlled through DeepChat tools: -- `set_recording` +- `start_recording` +- `stop_recording` - `get_recording_state` - `replay_trajectory` +- `install_ffmpeg` -Enable recording before UI actions, perform the same snapshot/action/verify loop, then inspect recording state. Replay only trajectories requested by the user or created in the current task. +Enable recording before UI actions, perform the same snapshot/action/verify loop, then inspect +recording state. Replay only trajectories requested by the user or created in the current task. + +`install_ffmpeg` may install or configure a dependency. Use it only after explicit user approval. diff --git a/plugins/cua/skills/cua-driver/SKILL.md b/plugins/cua/skills/cua-driver/SKILL.md index b3fd5072b..4dd4c9b23 100644 --- a/plugins/cua/skills/cua-driver/SKILL.md +++ b/plugins/cua/skills/cua-driver/SKILL.md @@ -1,68 +1,104 @@ --- name: cua-driver -description: Drive native macOS apps through DeepChat's plugin-provided Computer Use MCP tools. Use when the user asks to operate, inspect, automate, or perform a GUI task in a real macOS application. +description: Drive native desktop apps through DeepChat's built-in Computer Use tools. Use when the user asks to operate, inspect, automate, or perform a GUI task in a real desktop application. platforms: - darwin + - win32 + - linux metadata: deepchatFeature: computer-use --- # cua-driver -Use DeepChat's CUA plugin MCP tools for macOS app automation. Treat the tools exposed by the `cua-driver` MCP server as the only action surface for this skill. +Use DeepChat's plugin-provided Computer Use tools as the only action surface for this skill. Do not +ask the user to install `cua-driver`, configure an external server, or put anything on PATH for the +bundled DeepChat plugin. ## Runtime Context - Plugin id: `${OWNER_PLUGIN_ID}`. - Plugin root: `${PLUGIN_ROOT}`. -- Helper app bundle: `${PLUGIN_ROOT}/runtime/darwin/${PROCESS_ARCH}/DeepChat Computer Use.app`. -- Helper binary: `${PLUGIN_ROOT}/runtime/darwin/${PROCESS_ARCH}/DeepChat Computer Use.app/Contents/MacOS/cua-driver`. -- Permissions belong to the helper app bundle shown above. +- Process arch: `${PROCESS_ARCH}`. +- Supported targets: `darwin/arm64`, `darwin/x64`, `win32/x64`, `win32/arm64`, + `linux/x64`. +- Unsupported targets: `linux/arm64`. +- macOS helper app: `${PLUGIN_ROOT}/runtime/darwin/${PROCESS_ARCH}/CuaDriver.app`. +- Windows helper binary: `${PLUGIN_ROOT}/runtime/win32/${PROCESS_ARCH}/cua-driver.exe`. +- Linux helper binary: `${PLUGIN_ROOT}/runtime/linux/${PROCESS_ARCH}/cua-driver`. ## Required Loop -1. Resolve the app with `list_apps`. Match user language, localized names, English names, romanized names, bundle identifiers, and common abbreviations. Prefer `bundle_id` as the identity signal. -2. Start or reuse the target with `launch_app({ bundle_id })`. Use the returned `pid` when available. +1. Resolve the app with `list_apps`. Match localized names, English names, romanized names, bundle + identifiers, executable names, and common abbreviations. Prefer stable identifiers when a result + provides them. +2. Start or reuse the target with `launch_app`. Use the returned `pid` when available. 3. Inspect windows with `list_windows({ pid })` when the launch result lacks a usable window. -4. Snapshot before every UI action with `get_window_state({ pid, window_id })`. -5. Act with the matching MCP tool: `click`, `right_click`, `double_click`, `drag`, `scroll`, `type_text`, `press_key`, `hotkey`, `set_value`, `page`, or `launch_app` with `urls`. For web inputs that reject AX text insertion, call `type_text({ pid, text, delay_ms })` and let the driver use its CGEvent fallback. -6. Snapshot again after each action and verify visible evidence: selected state, changed text, playback progress, new panels, highlighted rows, or updated window content. - -Element indices come from the latest `get_window_state` result for the same `pid` and `window_id`. Re-snapshot when an index is missing, stale, or from another window. - -## Permissions - -Use `check_permissions` for permission status and prompting. If Accessibility or Screen Recording is missing, tell the user to grant it to `DeepChat Computer Use.app` from the helper app bundle path in this skill's runtime context. +4. Snapshot before every UI action with `get_window_state({ pid, window_id })`. Use a vision + capture mode when visual evidence is needed. +5. Act with the matching DeepChat tool: `click`, `right_click`, `double_click`, `drag`, `scroll`, + `type_text`, `press_key`, `hotkey`, `set_value`, `page`, or `launch_app` with URLs/files when + supported by the platform. +6. Snapshot again after each action and verify visible evidence: selected state, changed text, + playback progress, new panels, highlighted rows, or updated window content. + +Element indices come from the latest `get_window_state` result for the same `pid` and `window_id`. +Re-snapshot when an index is missing, stale, or from another window. + +## Platform Notes + +- macOS: use `check_permissions` for Accessibility and Screen Recording status. If a grant is + missing, ask the user to grant it to the staged `CuaDriver.app` helper. +- Windows: prefer background dispatch when available. Use `bring_to_front` only when foreground + interaction is necessary for the task. +- Linux: support is pre-release. Some compositors, sessions, and background interactions may be + unavailable. Use extra snapshots and report platform limits clearly when a tool cannot complete. ## Sparse UI Fallback -Many media and Electron apps expose a shallow accessibility tree while still showing actionable pixels. `get_window_state` automatically attempts Electron AX enablement through `AXManualAccessibility`, `AXEnhancedUserInterface`, and an AXObserver before returning a sparse-tree warning. +Many media, browser, and Electron apps expose a shallow accessibility tree while still showing +actionable pixels. Use this fallback order: 1. Re-snapshot once with `get_window_state({ pid, window_id })` when the first tree is sparse. -2. For Electron or browser-like windows, use `page` or relaunch with `launch_app({ bundle_id, electron_debugging_port: 9222 })` when DOM access would identify the target more reliably than pixels. -3. Use `screenshot({ window_id })` for broad visual confirmation when the window contents or active overlay are unclear. -4. Use at most one `zoom({ pid, window_id, x1, y1, x2, y2 })` for small text or dense icons. Repeated zoom calls are a failure signal; return to the full-window screenshot or ask for clarification. -5. Use pixel coordinates from the latest `get_window_state` screenshot with `click({ pid, window_id, x, y })`, or from the single zoom image with `click({ pid, window_id, x, y, from_zoom: true })`. +2. For browser-like windows, use `page` when DOM access identifies the target more reliably than + pixels. +3. Use `get_window_state` with vision capture for broad visual confirmation when window contents or + active overlays are unclear. +4. Use at most one `zoom({ pid, window_id, x1, y1, x2, y2 })` for small text or dense icons. + Repeated zoom calls are a failure signal; return to the full-window snapshot or ask for + clarification. +5. Use pixel coordinates from the latest same-window state with `click({ pid, window_id, x, y })`, + or from the single zoom image with `click({ pid, window_id, x, y, from_zoom: true })`. 6. Re-snapshot after each action and compare the resulting state. -Ask the user only when visible candidates are ambiguous, the requested action is destructive, or the target is outside the current visible window. +Ask the user only when visible candidates are ambiguous, the requested action is destructive, or the +target is outside the current visible window. ## Navigation Patterns -- For app launch: use `launch_app({ bundle_id })`. -- For opening files or URLs in an app: use `launch_app({ bundle_id, urls: [...] })`. -- For browser-like apps: prefer new windows via `launch_app({ bundle_id, urls: [...] })` so each URL has a stable `window_id`. -- For menu actions: use visible in-window controls first. Use menu-bar actions only when the target app is already the active app and the menu state is visible through the MCP snapshot. +- For app launch: use `launch_app`. +- For opening files or URLs in an app: use `launch_app` with the platform-supported file or URL + arguments. +- For browser-like apps: prefer new windows where possible so each URL has a stable `window_id`. +- For menu actions: use visible in-window controls first. Use menu-bar actions only when the target + app is active enough for the platform to expose menu state reliably. ## Agent Cursor -Use `get_agent_cursor_state` to inspect the cursor overlay. Use `set_agent_cursor_enabled`, `set_agent_cursor_motion`, or `set_agent_cursor_style` only when the user asks to show, hide, animate, or restyle the agent cursor. +Use `get_agent_cursor_state` to inspect the cursor overlay. Use `set_agent_cursor_enabled`, +`set_agent_cursor_motion`, or `set_agent_cursor_style` only when the user asks to show, hide, +animate, or restyle the agent cursor. + +## Recording + +Use `start_recording`, `stop_recording`, `get_recording_state`, and `replay_trajectory` for +recording workflows. Use `install_ffmpeg` only with explicit user approval. ## Linked References -- `README.md`: compact MCP workflow reference. +- `README.md`: compact workflow reference. - `WEB_APPS.md`: browser and webview patterns. - `RECORDING.md`: recording and replay tool notes. - `TESTS.md`: manual verification scenarios. diff --git a/plugins/cua/skills/cua-driver/TESTS.md b/plugins/cua/skills/cua-driver/TESTS.md index 7a281e89f..8f3e18f14 100644 --- a/plugins/cua/skills/cua-driver/TESTS.md +++ b/plugins/cua/skills/cua-driver/TESTS.md @@ -2,10 +2,11 @@ Use these checks after enabling the CUA plugin: -- `check_permissions` reports Accessibility and Screen Recording state for `DeepChat Computer Use.app`. -- `list_apps` returns installed macOS apps. -- `launch_app` starts a target app and returns a `pid`. +- `check_permissions` reports platform permission state or an explicit unavailable status. +- `list_apps` returns installed desktop apps. +- `launch_app` starts a target app and returns a `pid` when the platform can provide one. - `list_windows` returns windows for that `pid`. - `get_window_state` returns a screenshot or accessibility tree for a selected `window_id`. - `click` or `set_value` works after a same-window snapshot. -- Plugin disable removes the `cua-driver` tools after MCP refresh. +- `start_recording`, `stop_recording`, and `get_recording_state` are permission-gated. +- Plugin disable removes the `cua-driver` tools after the tool surface refreshes. diff --git a/plugins/cua/skills/cua-driver/WEB_APPS.md b/plugins/cua/skills/cua-driver/WEB_APPS.md index b4b28235a..734637bff 100644 --- a/plugins/cua/skills/cua-driver/WEB_APPS.md +++ b/plugins/cua/skills/cua-driver/WEB_APPS.md @@ -1,16 +1,19 @@ # Web App Patterns -Use `launch_app({ bundle_id, urls: [...] })` for browser and webview navigation. This creates a stable target that can be inspected with `list_windows` and `get_window_state`. +Use `launch_app` with URLs for browser and webview navigation when the target platform supports it. +This creates a stable target that can be inspected with `list_windows` and `get_window_state`. Recommended browser flow: 1. Launch the browser with the requested URL. 2. Select the relevant window from `list_windows`. 3. Snapshot with `get_window_state`. -4. Use `page` for supported browser/webview operations. +4. Use `page` for supported browser or webview operations. 5. Use visible UI tools for controls outside page automation. 6. Re-snapshot to verify state. -For Electron apps with sparse AX trees, prefer `page` after launching with `electron_debugging_port: 9222` when possible. If DOM access is unavailable, use `screenshot({ window_id })` for broad visual confirmation and one `zoom({ pid, window_id, ... })` only for small details before a window-local pixel click. +For Electron apps with sparse accessibility trees, prefer `page` when possible. If DOM access is +unavailable, use `get_window_state` with vision capture for broad visual confirmation and one +`zoom({ pid, window_id, ... })` only for small details before a window-local pixel click. For multiple URLs, prefer separate windows so each workflow keeps its own `window_id`. diff --git a/plugins/cua/vendor/cua-driver/upstream.json b/plugins/cua/vendor/cua-driver/upstream.json index ced2c26ff..252320c49 100644 --- a/plugins/cua/vendor/cua-driver/upstream.json +++ b/plugins/cua/vendor/cua-driver/upstream.json @@ -1,15 +1,31 @@ { - "sourceKind": "deepchat-owned-fork", + "sourceKind": "upstream-release", "upstreamRepo": "https://github.com/trycua/cua.git", - "upstreamSubdir": "libs/cua-driver", - "tag": "cua-driver-v0.2.0", - "commit": "d3f3b9325f49aa5302c15fb03f6b66bd1e688e27", - "version": "0.2.0", - "updatedAt": "2026-05-25", - "forkPolicy": "Build from the DeepChat-maintained local source snapshot. Cherry-pick upstream fixes only when they directly improve the bundled DeepChat Computer Use helper.", - "lastCherryPick": { - "sourceTag": "cua-driver-v0.2.0", - "sourceCommit": "d3f3b9325f49aa5302c15fb03f6b66bd1e688e27", - "appliedAt": "2026-05-25" - } + "upstreamSubdir": "libs/cua-driver/rust", + "tag": "cua-driver-rs-v0.5.5", + "commit": "d6dea4bc3c3a65ce821261752067cae8200fe5d6", + "version": "0.5.5", + "updatedAt": "2026-06-16", + "releaseUrl": "https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.5.5", + "checksumsAsset": "checksums.txt", + "supportedTargets": ["darwin/arm64", "darwin/x64", "win32/x64", "win32/arm64", "linux/x64"], + "unsupportedTargets": ["linux/arm64"], + "assets": { + "darwin-arm64": { + "name": "cua-driver-rs-0.5.5-darwin-arm64.tar.gz" + }, + "darwin-x64": { + "name": "cua-driver-rs-0.5.5-darwin-x86_64.tar.gz" + }, + "windows-x64": { + "name": "cua-driver-rs-0.5.5-windows-x86_64.zip" + }, + "windows-arm64": { + "name": "cua-driver-rs-0.5.5-windows-arm64.zip" + }, + "linux-x64": { + "name": "cua-driver-rs-0.5.5-linux-x86_64-binary.tar.gz" + } + }, + "releasePolicy": "Stage pinned upstream release assets at build time. Do not run upstream installers or require PATH-installed runtime binaries for the bundled DeepChat plugin." } diff --git a/scripts/build-cua-plugin-runtime.mjs b/scripts/build-cua-plugin-runtime.mjs index 7ba5eec4f..1ada4ad27 100644 --- a/scripts/build-cua-plugin-runtime.mjs +++ b/scripts/build-cua-plugin-runtime.mjs @@ -1,9 +1,11 @@ import { execFileSync, spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' import fs from 'node:fs/promises' import fsSync from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { unzipSync } from 'fflate' import { signMacHelperForRelease } from './sign-cua-helper.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -16,21 +18,22 @@ const pluginDir = process.env.DEEPCHAT_CUA_PLUGIN_DIR const vendorRoot = process.env.DEEPCHAT_CUA_VENDOR_ROOT ? path.resolve(process.env.DEEPCHAT_CUA_VENDOR_ROOT) : path.join(pluginDir, 'vendor', 'cua-driver') -const vendorSourceDir = path.join(vendorRoot, 'source') const upstreamMetadataPath = path.join(vendorRoot, 'upstream.json') -const helperAppName = 'DeepChat Computer Use' -const helperAppDirName = `${helperAppName}.app` const helperBinaryName = 'cua-driver' +const helperAppDirName = 'CuaDriver.app' + +const targetAssetKeys = { + 'darwin/arm64': 'darwin-arm64', + 'darwin/x64': 'darwin-x64', + 'win32/x64': 'windows-x64', + 'win32/arm64': 'windows-arm64', + 'linux/x64': 'linux-x64' +} -const archMap = { - arm64: { - swift: 'arm64', - lipo: 'arm64' - }, - x64: { - swift: 'x86_64', - lipo: 'x86_64' - } +const executableByTarget = { + darwin: path.join(helperAppDirName, 'Contents', 'MacOS', helperBinaryName), + win32: `${helperBinaryName}.exe`, + linux: helperBinaryName } function parseArgs(argv) { @@ -99,237 +102,321 @@ async function readUpstreamMetadata() { const requiredFields = [ 'sourceKind', 'upstreamRepo', - 'upstreamSubdir', 'tag', 'commit', 'version', 'updatedAt', - 'forkPolicy' + 'releaseUrl', + 'checksumsAsset' ] for (const field of requiredFields) { if (typeof metadata[field] !== 'string' || metadata[field].length === 0) { throw new Error(`CUA upstream metadata is missing required string field: ${field}`) } } - if (metadata.sourceKind !== 'deepchat-owned-fork') { - throw new Error(`CUA vendor sourceKind must be deepchat-owned-fork, got ${metadata.sourceKind}`) + if (metadata.sourceKind !== 'upstream-release') { + throw new Error(`CUA vendor sourceKind must be upstream-release, got ${metadata.sourceKind}`) + } + if (!metadata.assets || typeof metadata.assets !== 'object') { + throw new Error('CUA upstream metadata must declare release assets') } return metadata } -async function validateVendorSource(metadata) { - const packagePath = path.join(vendorSourceDir, 'Package.swift') - const sourcesPath = path.join(vendorSourceDir, 'Sources') - if (!(await pathExists(packagePath))) { - throw new Error(`Vendored CUA Driver source is missing Package.swift at ${packagePath}`) +function getTarget(platform, arch, metadata) { + const target = `${platform}/${arch}` + const assetKey = targetAssetKeys[target] + if (!assetKey) { + const unsupported = metadata.unsupportedTargets ?? [] + const reason = unsupported.includes(target) ? 'unsupported' : 'unknown' + throw new Error(`CUA plugin runtime target ${target} is ${reason}`) } - if (!(await pathExists(sourcesPath))) { - throw new Error(`Vendored CUA Driver source is missing Sources at ${sourcesPath}`) + + const asset = metadata.assets[assetKey] + if (!asset || typeof asset.name !== 'string') { + throw new Error(`CUA upstream metadata is missing asset mapping for ${target}`) } - const packageContent = await fs.readFile(packagePath, 'utf8') - if (!packageContent.includes('name: "cua-driver"')) { - throw new Error('Vendored CUA Driver Package.swift does not look like the cua-driver package') + return { + target, + assetKey, + assetName: asset.name } +} - const commandPath = path.join( - vendorSourceDir, - 'Sources', - 'CuaDriverCLI', - 'CuaDriverCommand.swift' - ) - const commandContent = await fs.readFile(commandPath, 'utf8') - if (!commandContent.includes('DeepChatPermissionProbeCommand')) { - throw new Error( - `Vendored CUA Driver source is missing DeepChat permission probe patch for ${metadata.commit}` - ) +function downloadUrl(metadata, assetName) { + return `https://github.com/trycua/cua/releases/download/${metadata.tag}/${assetName}` +} + +async function downloadFile(url, outputPath) { + if (await pathExists(outputPath)) { + return + } + + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`) + } + const buffer = Buffer.from(await response.arrayBuffer()) + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + await fs.writeFile(outputPath, buffer) +} + +async function sha256File(filePath) { + const hash = createHash('sha256') + hash.update(await fs.readFile(filePath)) + return hash.digest('hex') +} + +function parseChecksums(contents) { + const checksums = new Map() + for (const line of contents.split(/\r?\n/)) { + const match = line.trim().match(/^([a-f0-9]{64})\s+(.+)$/i) + if (match) { + checksums.set(match[2].trim(), match[1].toLowerCase()) + } + } + return checksums +} + +async function verifyChecksum(checksumsPath, assetPath, assetName) { + const checksums = parseChecksums(await fs.readFile(checksumsPath, 'utf8')) + const expected = checksums.get(assetName) + if (!expected) { + throw new Error(`checksums.txt does not contain ${assetName}`) + } + const actual = await sha256File(assetPath) + if (actual !== expected) { + throw new Error(`Checksum mismatch for ${assetName}. Expected ${expected}, got ${actual}`) } } -async function collectFiles(dir, extension) { +async function extractArchive(archivePath, outputDir) { + await fs.rm(outputDir, { recursive: true, force: true }) + await fs.mkdir(outputDir, { recursive: true }) + if (archivePath.endsWith('.zip')) { + const files = unzipSync(new Uint8Array(await fs.readFile(archivePath))) + for (const [relativePath, content] of Object.entries(files)) { + if (relativePath.endsWith('/')) { + continue + } + const normalized = relativePath.replace(/\\/g, '/') + if (normalized.startsWith('/') || normalized.includes('..') || /^[A-Za-z]:/.test(normalized)) { + throw new Error(`Unsafe CUA release archive path: ${relativePath}`) + } + const outputPath = path.resolve(outputDir, ...normalized.split('/').filter(Boolean)) + const relativeToRoot = path.relative(outputDir, outputPath) + if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { + throw new Error(`CUA release archive path escapes extraction root: ${relativePath}`) + } + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + await fs.writeFile(outputPath, Buffer.from(content)) + } + return + } + + ensureTool('tar', ['--version']) + run('tar', ['-xzf', archivePath, '-C', outputDir]) +} + +async function collectFiles(dir) { const entries = await fs.readdir(dir, { withFileTypes: true }) const files = [] for (const entry of entries) { const entryPath = path.join(dir, entry.name) if (entry.isDirectory()) { - files.push(...(await collectFiles(entryPath, extension))) - } else if (entry.isFile() && entry.name.endsWith(extension)) { + files.push(...(await collectFiles(entryPath))) + } else if (entry.isFile()) { files.push(entryPath) } } return files } -async function findBuiltBinary(scratchPath) { - const candidates = await collectFiles(scratchPath, '') - const binaries = candidates.filter((candidate) => path.basename(candidate) === helperBinaryName) - for (const candidate of binaries) { - const stat = await fs.stat(candidate) - if ((stat.mode & 0o111) !== 0 && candidate.includes(`${path.sep}release${path.sep}`)) { - return candidate +async function findFirst(root, predicate) { + const files = await collectFiles(root) + return files.find(predicate) +} + +async function findDirectory(root, directoryName) { + const entries = await fs.readdir(root, { withFileTypes: true }) + for (const entry of entries) { + const entryPath = path.join(root, entry.name) + if (entry.isDirectory() && entry.name === directoryName) { + return entryPath + } + if (entry.isDirectory()) { + const nested = await findDirectory(entryPath, directoryName) + if (nested) { + return nested + } } } - throw new Error('Built cua-driver binary was not found') + return undefined } -function plistXml(version) { - return ` - - - - CFBundleIdentifier - com.wefonk.deepchat.computeruse - CFBundleName - ${helperAppName} - CFBundleDisplayName - ${helperAppName} - CFBundleExecutable - ${helperBinaryName} - CFBundleIconFile - AppIcon - CFBundleIconName - AppIcon - CFBundlePackageType - APPL - CFBundleShortVersionString - ${version} - CFBundleVersion - ${version} - LSMinimumSystemVersion - 14.0 - LSUIElement - - NSHighResolutionCapable - - NSSupportsAutomaticTermination - - - -` +async function stageDarwinRuntime(extractDir, runtimeDir) { + const sourceApp = await findDirectory(extractDir, helperAppDirName) + if (!sourceApp) { + throw new Error(`CUA macOS archive is missing ${helperAppDirName}`) + } + await fs.cp(sourceApp, path.join(runtimeDir, helperAppDirName), { + recursive: true, + force: true + }) } -async function stageApp(sourceDir, builtBinary, targetArch, version) { - const runtimeDir = path.join(pluginDir, 'runtime', 'darwin', targetArch) - const helperAppPath = path.join(runtimeDir, helperAppDirName) - const contentsPath = path.join(helperAppPath, 'Contents') - const macosPath = path.join(contentsPath, 'MacOS') - const resourcesPath = path.join(contentsPath, 'Resources') - const stagedBinary = path.join(macosPath, helperBinaryName) +async function stageWindowsRuntime(extractDir, runtimeDir) { + const driver = await findFirst(extractDir, (file) => path.basename(file) === 'cua-driver.exe') + const uia = await findFirst(extractDir, (file) => path.basename(file) === 'cua-driver-uia.exe') + if (!driver || !uia) { + throw new Error('CUA Windows archive must contain cua-driver.exe and cua-driver-uia.exe') + } + await fs.copyFile(driver, path.join(runtimeDir, 'cua-driver.exe')) + await fs.copyFile(uia, path.join(runtimeDir, 'cua-driver-uia.exe')) +} + +async function stageLinuxRuntime(extractDir, runtimeDir) { + const driver = await findFirst( + extractDir, + (file) => path.basename(file) === 'cua-driver' && !file.endsWith('.exe') + ) + if (!driver) { + throw new Error('CUA Linux archive is missing cua-driver') + } + const target = path.join(runtimeDir, 'cua-driver') + await fs.copyFile(driver, target) + await fs.chmod(target, 0o755) +} +async function stageRuntime(targetPlatform, targetArch, extractDir) { + const runtimeDir = path.join(pluginDir, 'runtime', targetPlatform, targetArch) await fs.rm(runtimeDir, { recursive: true, force: true }) - await fs.mkdir(macosPath, { recursive: true }) - await fs.mkdir(resourcesPath, { recursive: true }) - await fs.copyFile(builtBinary, stagedBinary) - await fs.chmod(stagedBinary, 0o755) - await fs.writeFile(path.join(contentsPath, 'Info.plist'), plistXml(version)) + await fs.mkdir(runtimeDir, { recursive: true }) + + if (targetPlatform === 'darwin') { + await stageDarwinRuntime(extractDir, runtimeDir) + } else if (targetPlatform === 'win32') { + await stageWindowsRuntime(extractDir, runtimeDir) + } else if (targetPlatform === 'linux') { + await stageLinuxRuntime(extractDir, runtimeDir) + } else { + throw new Error(`Unsupported CUA runtime platform: ${targetPlatform}`) + } - const iconPath = path.join(sourceDir, 'App', 'CuaDriver', 'AppIcon.icns') - if (await pathExists(iconPath)) { - await fs.copyFile(iconPath, path.join(resourcesPath, 'AppIcon.icns')) + const executable = path.join(runtimeDir, executableByTarget[targetPlatform]) + if (!(await pathExists(executable))) { + throw new Error(`Staged CUA runtime is missing executable: ${executable}`) + } + if (targetPlatform !== 'win32') { + await fs.chmod(executable, 0o755) } + return { runtimeDir, executable } +} - validateArchitecture(stagedBinary, targetArch) - await signHelper(helperAppPath) - return helperAppPath +function canRunTarget(targetPlatform, targetArch) { + return process.platform === targetPlatform && process.arch === targetArch } -function validateArchitecture(binaryPath, targetArch) { - const expected = archMap[targetArch].lipo - const archs = read('/usr/bin/lipo', ['-archs', binaryPath]).split(/\s+/).filter(Boolean) +function smokeCheck(executable, targetPlatform, targetArch) { + if (!canRunTarget(targetPlatform, targetArch)) { + console.log(`Skipping CUA runtime smoke check for non-host target ${targetPlatform}/${targetArch}`) + return + } + + const result = spawnSync(executable, ['--version'], { + encoding: 'utf8', + windowsHide: true, + stdio: ['ignore', 'pipe', 'pipe'] + }) + if (result.error) { + throw result.error + } + if (result.status !== 0) { + throw new Error( + `CUA runtime smoke check failed with exit code ${result.status}: ${result.stderr || result.stdout}` + ) + } + console.log((result.stdout || result.stderr).trim()) +} + +function validateDarwinArchitecture(executable, targetArch) { + if (process.platform !== 'darwin') { + return + } + ensureTool('/usr/bin/lipo', ['-info', process.execPath]) + const expected = targetArch === 'x64' ? 'x86_64' : targetArch + const archs = read('/usr/bin/lipo', ['-archs', executable]).split(/\s+/).filter(Boolean) if (!archs.includes(expected)) { throw new Error(`Helper arch mismatch. Expected ${expected}, got ${archs.join(', ')}`) } } -async function signHelper(helperAppPath) { +async function signDarwinHelper(runtimeDir) { + if (process.platform !== 'darwin') { + return + } + ensureTool('codesign', ['--version']) + const helperAppPath = path.join(runtimeDir, helperAppDirName) const entitlementsPath = path.join(pluginDir, 'build', 'entitlements.plist') const signedForRelease = await signMacHelperForRelease({ appPath: helperAppPath, entitlementsPath, cwd: rootDir }) - if (signedForRelease) { - return + if (!signedForRelease) { + run('codesign', [ + '--force', + '--deep', + '--sign', + '-', + '--entitlements', + entitlementsPath, + '--options', + 'runtime', + '--timestamp=none', + helperAppPath + ]) } - - run('codesign', [ - '--force', - '--deep', - '--sign', - '-', - '--entitlements', - entitlementsPath, - '--options', - 'runtime', - '--timestamp=none', - helperAppPath - ]) run('codesign', ['--verify', '--deep', '--strict', '--verbose=2', helperAppPath]) } async function main() { const args = parseArgs(process.argv.slice(2)) - const requestedArch = args.get('arch') ?? process.env.TARGET_ARCH ?? process.arch - - if (process.platform !== 'darwin') { - throw new Error('CUA plugin runtime build requires macOS.') - } - - if (!archMap[requestedArch]) { - throw new Error(`Unsupported CUA Driver arch: ${requestedArch}`) - } - - ensureTool('swift') - ensureTool('/usr/bin/lipo', ['-info', process.execPath]) - ensureTool('codesign', ['--version']) - + const targetPlatform = args.get('platform') ?? process.env.TARGET_PLATFORM ?? process.platform + const targetArch = args.get('arch') ?? process.env.TARGET_ARCH ?? process.arch const metadata = await readUpstreamMetadata() - await validateVendorSource(metadata) - + const target = getTarget(targetPlatform, targetArch, metadata) + const cacheDir = process.env.DEEPCHAT_CUA_DOWNLOAD_CACHE + ? path.resolve(process.env.DEEPCHAT_CUA_DOWNLOAD_CACHE) + : path.join(os.tmpdir(), 'deepchat-cua-driver-cache', metadata.tag) const workRoot = path.join( os.tmpdir(), 'deepchat-cua-plugin-build', - `${metadata.tag}-${requestedArch}-${process.pid}` + `${metadata.tag}-${targetPlatform}-${targetArch}-${process.pid}` ) - const scratchPath = path.join(workRoot, '.build', requestedArch) - - await fs.rm(workRoot, { recursive: true, force: true }) - await fs.mkdir(workRoot, { recursive: true }) - - run( - 'swift', - [ - 'build', - '-c', - 'release', - '--arch', - archMap[requestedArch].swift, - '--product', - helperBinaryName, - '--package-path', - vendorSourceDir, - '--scratch-path', - scratchPath - ], - { - env: { - ...process.env, - CUA_DRIVER_TELEMETRY_ENABLED: '0', - CUA_DRIVER_AUTO_UPDATE_ENABLED: '0' - } - } - ) - - const builtBinary = await findBuiltBinary(scratchPath) - const helperAppPath = await stageApp(vendorSourceDir, builtBinary, requestedArch, metadata.version) - const relativeHelperPath = path.relative(rootDir, helperAppPath) - const stat = await fs.stat(path.join(helperAppPath, 'Contents', 'MacOS', helperBinaryName)) - - if (!fsSync.existsSync(helperAppPath) || stat.size === 0) { - throw new Error('Staged helper app is invalid') + const extractDir = path.join(workRoot, 'extract') + const assetPath = path.join(cacheDir, target.assetName) + const checksumsPath = path.join(cacheDir, metadata.checksumsAsset) + + await downloadFile(downloadUrl(metadata, metadata.checksumsAsset), checksumsPath) + await downloadFile(downloadUrl(metadata, target.assetName), assetPath) + await verifyChecksum(checksumsPath, assetPath, target.assetName) + await extractArchive(assetPath, extractDir) + + const { runtimeDir, executable } = await stageRuntime(targetPlatform, targetArch, extractDir) + validateDarwinArchitecture(executable, targetArch) + await signDarwinHelper(runtimeDir) + smokeCheck(executable, targetPlatform, targetArch) + + const relativeRuntimePath = path.relative(rootDir, runtimeDir) + const stat = await fs.stat(executable) + if (!fsSync.existsSync(executable) || stat.size === 0) { + throw new Error('Staged CUA runtime is invalid') } - console.log(`CUA Driver ${metadata.tag} staged at ${relativeHelperPath}`) + await fs.rm(workRoot, { recursive: true, force: true }) + console.log(`CUA Driver ${metadata.tag} staged at ${relativeRuntimePath}`) } main().catch((error) => { diff --git a/scripts/package-plugin.mjs b/scripts/package-plugin.mjs index 9545516a3..2f1ddd60e 100644 --- a/scripts/package-plugin.mjs +++ b/scripts/package-plugin.mjs @@ -17,7 +17,7 @@ function parseArgs(argv) { pluginDir: null, releaseVersionFromRoot: false, version: null, - targetPlatform: process.env.TARGET_PLATFORM ?? null, + targetPlatform: process.env.TARGET_PLATFORM ?? process.platform, targetArch: process.env.TARGET_ARCH ?? process.arch } @@ -115,6 +115,12 @@ function validateManifest(pluginDir, manifest) { if (!Array.isArray(manifest.engines?.platforms) || manifest.engines.platforms.length === 0) { throw new Error('engines.platforms must declare at least one platform') } + if ( + manifest.engines.targets !== undefined && + (!Array.isArray(manifest.engines.targets) || manifest.engines.targets.length === 0) + ) { + throw new Error('engines.targets must be a non-empty array when declared') + } for (const skill of manifest.skills ?? []) { assertFile(pluginDir, skill.path, `skill ${skill.id}`) @@ -132,8 +138,8 @@ function shouldSkipPackageEntry(relativePath, manifest, args) { } const parts = relativePath.split('/') - if (parts[0] === 'runtime' && parts[1] === 'darwin' && parts[2]) { - return parts[2] !== args.targetArch + if (parts[0] === 'runtime' && parts[1] && parts[2]) { + return parts[1] !== args.targetPlatform || parts[2] !== args.targetArch } return false @@ -163,7 +169,11 @@ function collectFiles(pluginDir, currentDir = pluginDir, files = {}, manifest, a continue } - files[relativePath] = new Uint8Array(fs.readFileSync(absolutePath)) + const stat = fs.statSync(absolutePath) + files[relativePath] = { + content: new Uint8Array(fs.readFileSync(absolutePath)), + mode: stat.mode + } } return files } @@ -203,25 +213,62 @@ function createPackageManifest(manifest, args) { return next } +function targetKey(targetPlatform, targetArch) { + return `${targetPlatform}/${targetArch}` +} + +function isManifestTargetSupported(manifest, targetPlatform, targetArch) { + const aliases = targetPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [targetPlatform] + const targets = manifest.engines?.targets ?? [] + if (targets.length > 0) { + const supportedTargets = targets.map((target) => String(target).toLowerCase()) + return aliases.some((platform) => supportedTargets.includes(`${platform}/${targetArch}`)) + } + + const platforms = new Set( + (manifest.engines?.platforms ?? []).map((platform) => String(platform).toLowerCase()) + ) + return aliases.some((platform) => platforms.has(platform)) +} + function validateCuaRuntime(pluginDir, manifest, args) { if (manifest.id !== 'com.deepchat.plugins.cua') { return } - const targetPlatform = args.targetPlatform ?? 'darwin' - if (targetPlatform !== 'darwin') { - throw new Error('CUA plugin packaging currently supports darwin runtime packages only') + const targetPlatform = args.targetPlatform ?? process.platform + const key = targetKey(targetPlatform, args.targetArch) + if (!isManifestTargetSupported(manifest, targetPlatform, args.targetArch)) { + throw new Error(`CUA plugin does not support ${key}`) } - assertFile( - pluginDir, - `runtime/darwin/${args.targetArch}/DeepChat Computer Use.app/Contents/MacOS/cua-driver`, - `CUA runtime binary ${targetPlatform}/${args.targetArch}` - ) + + const requiredByTarget = { + [`darwin/${args.targetArch}`]: [ + `runtime/darwin/${args.targetArch}/CuaDriver.app/Contents/MacOS/cua-driver` + ], + [`win32/${args.targetArch}`]: [ + `runtime/win32/${args.targetArch}/cua-driver.exe`, + `runtime/win32/${args.targetArch}/cua-driver-uia.exe` + ], + [`linux/${args.targetArch}`]: [`runtime/linux/${args.targetArch}/cua-driver`] + } + const requiredFiles = requiredByTarget[key] + if (!requiredFiles) { + throw new Error(`CUA plugin has no runtime validation rule for ${key}`) + } + for (const relativePath of requiredFiles) { + assertFile(pluginDir, relativePath, `CUA runtime binary ${key}`) + } + const expectedDetect = [ - `plugin:runtime/darwin/${args.targetArch}/DeepChat Computer Use.app/Contents/MacOS/cua-driver`, + `plugin:runtime/darwin/${args.targetArch}/CuaDriver.app/Contents/MacOS/cua-driver`, + `plugin:runtime/win32/${args.targetArch}/cua-driver.exe`, + `plugin:runtime/linux/${args.targetArch}/cua-driver`, '/Applications/CuaDriver.app/Contents/MacOS/cua-driver' ] - if (JSON.stringify(manifest.runtime?.detect ?? []) !== JSON.stringify(expectedDetect)) { - throw new Error('CUA runtime detect paths must point to the bundled helper app first') + for (const detectPath of expectedDetect) { + if (!manifest.runtime?.detect?.includes(detectPath)) { + throw new Error(`CUA runtime detect paths must include ${detectPath}`) + } } const cuaServer = (manifest.mcpServers ?? []).find((server) => server.id === 'cua-driver') @@ -250,21 +297,37 @@ function buildChecksums(files) { .sort(([left], [right]) => left.localeCompare(right)) .map(([filePath, content]) => [ filePath, - createHash('sha256').update(Buffer.from(content)).digest('hex') + createHash('sha256').update(Buffer.from(content.content)).digest('hex') ]) ) } +function createZipInput(files) { + return Object.fromEntries( + Object.entries(files).map(([filePath, file]) => { + const mode = file.mode & 0o777 + if ((mode & 0o111) !== 0) { + return [filePath, [file.content, { os: 3, attrs: mode << 16 }]] + } + return [filePath, file.content] + }) + ) +} + function packagePlugin(pluginDir, outDir, manifest, args) { const files = collectFiles(pluginDir, pluginDir, {}, manifest, args) - files['plugin.json'] = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`) - files['checksums.json'] = new TextEncoder().encode( - `${JSON.stringify(buildChecksums(files), null, 2)}\n` - ) + files['plugin.json'] = { + content: new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`), + mode: 0o644 + } + files['checksums.json'] = { + content: new TextEncoder().encode(`${JSON.stringify(buildChecksums(files), null, 2)}\n`), + mode: 0o644 + } fs.mkdirSync(outDir, { recursive: true }) const outPath = path.join(outDir, artifactFileName(manifest, args.targetPlatform, args.targetArch)) - fs.writeFileSync(outPath, Buffer.from(zipSync(files, { level: 6 }))) + fs.writeFileSync(outPath, Buffer.from(zipSync(createZipInput(files), { level: 6 }))) return outPath } @@ -273,6 +336,9 @@ try { const sourceManifest = readManifest(args.pluginDir) const manifest = createPackageManifest(sourceManifest, args) validateManifest(args.pluginDir, manifest) + if (!isManifestTargetSupported(manifest, args.targetPlatform, args.targetArch)) { + throw new Error(`Plugin ${manifest.id} does not support ${targetKey(args.targetPlatform, args.targetArch)}`) + } validateCuaRuntime(args.pluginDir, manifest, args) if (args.validateOnly) { console.log(`Plugin ${manifest.id}@${manifest.version} is valid`) diff --git a/scripts/plugin.mjs b/scripts/plugin.mjs index cdc3f52b8..aebdefa90 100644 --- a/scripts/plugin.mjs +++ b/scripts/plugin.mjs @@ -71,7 +71,8 @@ function discoverOfficialPlugins() { return { name: entry.name, manifest, - platforms: manifest.engines?.platforms ?? [] + platforms: manifest.engines?.platforms ?? [], + targets: manifest.engines?.targets ?? [] } } catch { return null @@ -81,9 +82,13 @@ function discoverOfficialPlugins() { .sort((a, b) => a.name.localeCompare(b.name)) } -function isPluginSupported(plugin, targetPlatform) { +function isPluginSupported(plugin, targetPlatform, targetArch) { const platforms = new Set(plugin.platforms.map((platform) => String(platform).toLowerCase())) const aliases = targetPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [targetPlatform] + const targets = plugin.targets.map((target) => String(target).toLowerCase()) + if (targets.length > 0) { + return aliases.some((platform) => targets.includes(`${platform}/${targetArch}`)) + } return aliases.some((platform) => platforms.has(platform)) } @@ -109,7 +114,9 @@ function verifyArtifacts(options) { throw new Error(`Official plugin not found: ${options.name}`) } - const expected = selected.filter((plugin) => isPluginSupported(plugin, options.platform)) + const expected = selected.filter((plugin) => + isPluginSupported(plugin, options.platform, options.arch) + ) if (expected.length === 0) { throw new Error(`No official plugins are expected for ${options.platform}/${options.arch}`) } @@ -136,6 +143,7 @@ try { const nativeBuildScript = path.resolve(`scripts/build-${args.name}-plugin-runtime.mjs`) if (args.action === 'bundle' && existsSync(nativeBuildScript)) { const buildArgs = [nativeBuildScript] + if (args.platform) buildArgs.push('--platform', args.platform) if (args.arch) buildArgs.push('--arch', args.arch) execFileSync('node', buildArgs, { stdio: 'inherit' }) } diff --git a/src/main/presenter/pluginPresenter/index.ts b/src/main/presenter/pluginPresenter/index.ts index 690c17b90..04b873222 100644 --- a/src/main/presenter/pluginPresenter/index.ts +++ b/src/main/presenter/pluginPresenter/index.ts @@ -42,6 +42,7 @@ type PluginPresenterDeps = { mcpPresenter: IMCPPresenter skillPresenter: ISkillPresenter platform?: NodeJS.Platform + arch?: NodeJS.Architecture appPath?: string isPackaged?: boolean resourcesPath?: string @@ -80,6 +81,7 @@ export class PluginPresenter { private readonly mcpPresenter: IMCPPresenter private readonly skillPresenter: SkillContributionPort private readonly platform: NodeJS.Platform + private readonly arch: NodeJS.Architecture private readonly appPath: string private readonly isPackaged: boolean private readonly resourcesPath: string @@ -99,6 +101,7 @@ export class PluginPresenter { this.mcpPresenter = deps.mcpPresenter this.skillPresenter = deps.skillPresenter as SkillContributionPort this.platform = deps.platform ?? process.platform + this.arch = deps.arch ?? process.arch this.appPath = deps.appPath ?? app.getAppPath() this.isPackaged = deps.isPackaged ?? app.isPackaged this.resourcesPath = deps.resourcesPath ?? process.resourcesPath ?? '' @@ -583,6 +586,10 @@ export class PluginPresenter { } } + if (this.platform !== 'darwin') { + return await this.runRuntimePermissionToolFallback(pluginId, runtime.command) + } + try { return await this.runRuntimePermissionProbe(pluginId, runtime.command) } catch (probeError) { @@ -645,7 +652,7 @@ export class PluginPresenter { private async runRuntimePermissionToolFallback( pluginId: string, command: string, - probeError: unknown + probeError?: unknown ): Promise { try { const { stdout, stderr } = await execFileAsync(command, ['check_permissions'], { @@ -653,14 +660,17 @@ export class PluginPresenter { windowsHide: true }) const output = `${stdout}\n${stderr}` - return { + const result: RuntimePermissionCheckResult = { accessibility: this.parsePermissionState(output, 'Accessibility'), screenRecording: this.parsePermissionState(output, 'Screen Recording'), command, stdout: this.truncateOutput(stdout), - stderr: this.truncateOutput(stderr), - error: `Permission probe failed; used fallback. ${this.describeError(probeError)}` + stderr: this.truncateOutput(stderr) } + if (probeError) { + result.error = `Permission probe failed; used fallback. ${this.describeError(probeError)}` + } + return result } catch (error) { console.warn('[PluginHost] Runtime permission fallback failed:', { pluginId, @@ -986,13 +996,17 @@ export class PluginPresenter { private assertPlatformSupported(manifest: DeepChatPluginManifest): void { if (!this.isPluginPlatformSupported(manifest)) { - throw new Error(`Plugin ${manifest.id} does not support ${this.platform}`) + throw new Error(`Plugin ${manifest.id} does not support ${this.platform}/${this.arch}`) } } private isPluginPlatformSupported(manifest: DeepChatPluginManifest): boolean { const platforms = new Set(manifest.engines.platforms.map((platform) => platform.toLowerCase())) const aliases = this.platform === 'darwin' ? ['darwin', 'macos', 'mac'] : [this.platform] + const targets = manifest.engines.targets?.map((target) => target.toLowerCase()) ?? [] + if (targets.length > 0) { + return aliases.some((platform) => targets.includes(`${platform}/${this.arch}`)) + } return aliases.some((platform) => platforms.has(platform)) } @@ -1361,7 +1375,7 @@ export class PluginPresenter { } private resolveRuntimeCandidate(candidate: string, pluginRoot: string): string | null { - candidate = candidate.replaceAll('${arch}', process.arch) + candidate = candidate.replaceAll('${arch}', this.arch) if (candidate.startsWith('plugin:')) { return this.resolvePluginRelativePath(pluginRoot, candidate.slice('plugin:'.length)) } @@ -1448,7 +1462,7 @@ export class PluginPresenter { return JSON.parse( JSON.stringify(manifest) .replaceAll('${app.version}', app.getVersion()) - .replaceAll('${arch}', process.arch) + .replaceAll('${arch}', this.arch) .replaceAll('${target.platform}', this.platform) .replaceAll( '${github.release.download}', diff --git a/src/shared/types/plugin.ts b/src/shared/types/plugin.ts index 7ea4f7921..22c05c805 100644 --- a/src/shared/types/plugin.ts +++ b/src/shared/types/plugin.ts @@ -20,6 +20,7 @@ export type PluginToolPolicyDecision = 'allow' | 'ask' | 'deny' export interface PluginEngineManifest { deepchat: string platforms: string[] + targets?: string[] } export interface PluginSourceManifest { diff --git a/test/main/presenter/pluginPresenter.test.ts b/test/main/presenter/pluginPresenter.test.ts index bd07b94a8..3ed27a032 100644 --- a/test/main/presenter/pluginPresenter.test.ts +++ b/test/main/presenter/pluginPresenter.test.ts @@ -41,6 +41,7 @@ type CreatePluginPresenterOptions = { isPackaged?: boolean resourcesPath?: string mcpEnabled?: boolean + arch?: NodeJS.Architecture } const createPluginPresenter = async ( @@ -75,6 +76,7 @@ const createPluginPresenter = async ( } const presenter = new PluginPresenter({ platform, + arch: options.arch, appPath: options.appPath ?? process.cwd(), isPackaged: options.isPackaged, resourcesPath: options.resourcesPath, @@ -106,7 +108,8 @@ const createBundledFixture = async ( const userDataPath = path.join(root, 'userData') const packageRoot = options.packageRoot ?? path.join(appPath, 'plugins') const packagePath = path.join(packageRoot, 'deepchat-plugin-fixture-0.2.3-darwin-x64.dcplugin') - const runtimeRelativePath = `runtime/darwin/${process.arch}/fixture-runtime` + const runtimeFileName = process.platform === 'win32' ? 'fixture-runtime.cmd' : 'fixture-runtime' + const runtimeRelativePath = `runtime/darwin/${process.arch}/${runtimeFileName}` const pluginId = options.pluginId ?? 'com.deepchat.plugins.fixture' const includeSettings = options.includeSettings ?? false const manifest = { @@ -131,7 +134,7 @@ const createBundledFixture = async ( id: 'fixture-runtime', type: 'external-helper', displayName: 'Fixture Runtime', - detect: [`plugin:${runtimeRelativePath}`] + detect: [`PATH:${process.execPath}`] }, mcpServers: [ { @@ -159,7 +162,11 @@ const createBundledFixture = async ( } const files: Record = { 'plugin.json': new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`), - [runtimeRelativePath]: new TextEncoder().encode('#!/bin/sh\necho fixture-runtime 1.0.0\n') + [runtimeRelativePath]: new TextEncoder().encode( + process.platform === 'win32' + ? '@echo off\r\necho fixture-runtime 1.0.0\r\n' + : '#!/bin/sh\necho fixture-runtime 1.0.0\n' + ) } if (includeSettings) { files['settings/index.html'] = new TextEncoder().encode( @@ -317,7 +324,7 @@ describe('PluginPresenter', () => { await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) }) - it('hides the CUA official plugin on unsupported platforms', async () => { + it('uses CUA target metadata to show only supported platform and arch pairs', async () => { const root = await mkdtemp(path.join(os.tmpdir(), 'deepchat-plugin-platform-test-')) tempRoots.push(root) const userDataPath = path.join(root, 'userData') @@ -326,15 +333,30 @@ describe('PluginPresenter', () => { name === 'userData' ? userDataPath : path.join(root, name) ) - const winPresenter = await createPluginPresenter('win32') - const linuxPresenter = await createPluginPresenter('linux') + const winX64Presenter = await createPluginPresenter('win32', { arch: 'x64' }) + const winArmPresenter = await createPluginPresenter('win32', { arch: 'arm64' }) + const linuxX64Presenter = await createPluginPresenter('linux', { arch: 'x64' }) + const linuxArmPresenter = await createPluginPresenter('linux', { arch: 'arm64' }) const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) - expect(manifest.engines.platforms).toEqual(['darwin']) - expect((await winPresenter.listPlugins()).map((plugin) => plugin.id)).not.toContain( + expect(manifest.engines.platforms).toEqual(['darwin', 'win32', 'linux']) + expect(manifest.engines.targets).toEqual([ + 'darwin/arm64', + 'darwin/x64', + 'win32/x64', + 'win32/arm64', + 'linux/x64' + ]) + expect((await winX64Presenter.listPlugins()).map((plugin) => plugin.id)).toContain( + 'com.deepchat.plugins.cua' + ) + expect((await linuxX64Presenter.listPlugins()).map((plugin) => plugin.id)).toContain( 'com.deepchat.plugins.cua' ) - expect((await linuxPresenter.listPlugins()).map((plugin) => plugin.id)).not.toContain( + expect((await winArmPresenter.listPlugins()).map((plugin) => plugin.id)).toContain( + 'com.deepchat.plugins.cua' + ) + expect((await linuxArmPresenter.listPlugins()).map((plugin) => plugin.id)).not.toContain( 'com.deepchat.plugins.cua' ) }) @@ -361,7 +383,7 @@ describe('PluginPresenter', () => { enabled: true, runtime: { state: 'installed', - version: 'fixture-runtime 1.0.0' + version: process.version } }) expect( @@ -579,11 +601,13 @@ describe('PluginPresenter', () => { expect(fs.existsSync(path.join(fixture.installedRoot, 'mcp', 'legacy.mjs'))).toBe(false) expect(configAfterRefresh).toMatchObject(config) expect(servers['fixture-tools']).toMatchObject({ - args: [path.join(fixture.installedRoot, 'mcp', 'serve.mjs')], source: 'plugin', sourceId: fixture.pluginId, enabled: true }) + expect(servers['fixture-tools'].args.map((arg: string) => path.normalize(arg))).toEqual([ + path.join(fixture.installedRoot, 'mcp', 'serve.mjs') + ]) expect(presenter.__mocks.mcpPresenter.startServer).toHaveBeenCalledWith('fixture-tools') }) @@ -744,16 +768,15 @@ describe('PluginPresenter', () => { expect(presenter.__mocks.mcpPresenter.startServer).toHaveBeenCalledWith('fixture-runtime') }) - it('declares the CUA MCP server with plugin helper context', async () => { + it('declares the CUA internal tool server with cross-platform helper context', async () => { const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) const mcpConfig = JSON.parse(await readFile('plugins/cua/mcp/cua-driver.json', 'utf8')) const server = manifest.mcpServers.find((item: { id: string }) => item.id === 'cua-driver') - expect(manifest.runtime.detect[0]).toBe( - 'plugin:runtime/darwin/${arch}/DeepChat Computer Use.app/Contents/MacOS/cua-driver' - ) expect(manifest.runtime.detect).toEqual([ - 'plugin:runtime/darwin/${arch}/DeepChat Computer Use.app/Contents/MacOS/cua-driver', + 'plugin:runtime/darwin/${arch}/CuaDriver.app/Contents/MacOS/cua-driver', + 'plugin:runtime/win32/${arch}/cua-driver.exe', + 'plugin:runtime/linux/${arch}/cua-driver', '/Applications/CuaDriver.app/Contents/MacOS/cua-driver' ]) expect(server.env).toEqual({ @@ -764,139 +787,113 @@ describe('PluginPresenter', () => { expect(mcpConfig.env).toEqual(server.env) }) - it('keeps new CUA cursor style controls permission-gated', async () => { + it('keeps CUA v0.5.5 tool policies explicit and conservative', async () => { const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) const policy = JSON.parse(await readFile('plugins/cua/policies/tool-policy.json', 'utf8')) - const registrySource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/ToolRegistry.swift', - 'utf8' - ) - const styleToolSource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/SetAgentCursorStyleTool.swift', - 'utf8' - ) const manifestTools = manifest.toolPolicies.find( (item: { serverId: string }) => item.serverId === 'cua-driver' ).tools + const expectedAllow = [ + 'check_permissions', + 'list_apps', + 'list_windows', + 'get_screen_size', + 'get_window_state', + 'get_accessibility_tree', + 'get_cursor_position', + 'get_config', + 'get_recording_state', + 'get_agent_cursor_state', + 'check_for_update', + 'debug_window_info', + 'start_session', + 'end_session' + ] + const expectedAsk = [ + 'launch_app', + 'kill_app', + 'bring_to_front', + 'click', + 'right_click', + 'double_click', + 'drag', + 'mouse_button_down', + 'mouse_button_up', + 'mouse_drag', + 'parallel_mouse_drag', + 'scroll', + 'move_cursor', + 'type_text', + 'type_text_chars', + 'press_key', + 'hotkey', + 'set_value', + 'set_config', + 'start_recording', + 'stop_recording', + 'install_ffmpeg', + 'set_agent_cursor_enabled', + 'set_agent_cursor_motion', + 'set_agent_cursor_style', + 'replay_trajectory', + 'zoom', + 'page' + ] + + for (const tool of expectedAllow) { + expect(manifestTools[tool]).toBe('allow') + expect(policy.tools[tool]).toBe('allow') + } + for (const tool of expectedAsk) { + expect(manifestTools[tool]).toBe('ask') + expect(policy.tools[tool]).toBe('ask') + } - expect(styleToolSource).toContain('name: "set_agent_cursor_style"') - expect(registrySource).toContain('SetAgentCursorStyleTool.handler') - expect(manifestTools.set_agent_cursor_style).toBe('ask') - expect(policy.tools.set_agent_cursor_style).toBe('ask') + expect(manifestTools.screenshot).toBeUndefined() + expect(manifestTools.set_recording).toBeUndefined() + expect(policy.tools.screenshot).toBeUndefined() + expect(policy.tools.set_recording).toBeUndefined() }) - it('tracks CUA vendor source as a DeepChat-owned fork', async () => { + it('tracks CUA as a pinned upstream release asset set', async () => { const metadata = JSON.parse( await readFile('plugins/cua/vendor/cua-driver/upstream.json', 'utf8') ) const buildScript = await readFile('scripts/build-cua-plugin-runtime.mjs', 'utf8') expect(metadata).toMatchObject({ - sourceKind: 'deepchat-owned-fork', + sourceKind: 'upstream-release', upstreamRepo: 'https://github.com/trycua/cua.git', - upstreamSubdir: 'libs/cua-driver' + upstreamSubdir: 'libs/cua-driver/rust', + tag: 'cua-driver-rs-v0.5.5', + commit: 'd6dea4bc3c3a65ce821261752067cae8200fe5d6', + version: '0.5.5', + supportedTargets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64'], + unsupportedTargets: ['linux/arm64'] }) - expect(metadata.forkPolicy).toContain('Cherry-pick upstream fixes') - expect(metadata.lastCherryPick).toMatchObject({ - sourceTag: metadata.tag, - sourceCommit: metadata.commit - }) - expect(buildScript).toContain('vendorSourceDir') + expect(metadata.assets['windows-x64'].name).toBe('cua-driver-rs-0.5.5-windows-x86_64.zip') + expect(metadata.assets['windows-arm64'].name).toBe('cua-driver-rs-0.5.5-windows-arm64.zip') + expect(metadata.assets['linux-x64'].name).toBe('cua-driver-rs-0.5.5-linux-x86_64-binary.tar.gz') + expect(buildScript).toContain('verifyChecksum') + expect(buildScript).toContain('downloadFile') expect(buildScript).toContain('sourceKind') - expect(buildScript).toContain('deepchat-owned-fork') - expect(buildScript).toContain('--package-path') - expect(buildScript).toContain('vendorSourceDir') - }) - - it('keeps CUA updates managed by DeepChat instead of upstream release checks', async () => { - const commandSource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverCLI/CuaDriverCommand.swift', - 'utf8' - ) - - expect(commandSource).toContain('DeepChat packages this cua-driver fork with the app.') - expect(commandSource).toContain('Update DeepChat to receive newer Computer Use helper builds.') - expect(commandSource).not.toContain('VersionCheck.fetchLatest') - expect(commandSource).not.toContain('Could not reach GitHub') - expect(commandSource).not.toContain('Checking for updates') + expect(buildScript).toContain('upstream-release') + expect(buildScript).not.toContain('swift') + expect(buildScript).not.toContain('--package-path') }) - it('keeps CUA default pixel clicks on the upstream auth-signed path', async () => { - const mouseInput = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverCore/Input/MouseInput.swift', - 'utf8' - ) - - expect(mouseInput).toContain('try clickViaAuthSignedPost(') - expect(mouseInput).toContain('private static func clickViaAuthSignedPost') - expect(mouseInput).not.toContain('clickViaBackgroundPidPost') - }) - - it('scopes CUA zoom contexts to pid and window_id', async () => { - const registrySource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/ImageResizeRegistry.swift', - 'utf8' - ) - const zoomTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/ZoomTool.swift', - 'utf8' - ) - const clickTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/ClickTool.swift', - 'utf8' - ) - const dragTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/DragTool.swift', - 'utf8' - ) - const stateTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift', - 'utf8' - ) - - expect(registrySource).toContain('public struct ImageContextKey') - expect(registrySource).toContain('private var ratios: [ImageContextKey: Double]') - expect(registrySource).toContain('private var zooms: [ImageContextKey: ZoomContext]') - expect(zoomTool).toContain('"window_id"') - expect(zoomTool).toContain('capture.captureWindow') - expect(zoomTool).toContain('windowId: windowId') - expect(stateTool).toContain('setRatio(') - expect(stateTool).toContain('windowId: windowId') - for (const source of [clickTool, dragTool]) { - expect(source).toContain('from_zoom=true but no zoom context for pid') - expect(source).toContain('Call `zoom` with the same pid and window_id first.') - expect(source).toContain('windowId: windowId') - } - }) - - it('keeps Electron AX enablement internal instead of adding a public tool', async () => { + it('keeps unreviewed CUA tools out of the policy surface', async () => { const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) const policy = JSON.parse(await readFile('plugins/cua/policies/tool-policy.json', 'utf8')) - const registrySource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/ToolRegistry.swift', - 'utf8' - ) - const stateSource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverCore/AppState/AppState.swift', - 'utf8' - ) - const enablementSource = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverCore/Focus/AXEnablementAssertion.swift', - 'utf8' - ) const manifestTools = manifest.toolPolicies.find( (item: { serverId: string }) => item.serverId === 'cua-driver' ).tools - expect(registrySource).not.toContain('SetElectronAccessibilityTool') expect(manifestTools.set_electron_accessibility).toBeUndefined() expect(policy.tools.set_electron_accessibility).toBeUndefined() - expect(stateSource).toContain('activateAccessibilityIfNeeded') - expect(enablementSource).toContain('AXManualAccessibility') - expect(enablementSource).toContain('AXEnhancedUserInterface') }) - it('keeps the CUA skill instructions MCP-only', async () => { + it('keeps the CUA skill instructions aligned with DeepChat bundled tools', async () => { const files = ['SKILL.md', 'README.md', 'WEB_APPS.md', 'RECORDING.md', 'TESTS.md'] const contents = await Promise.all( files.map((file) => readFile(`plugins/cua/skills/cua-driver/${file}`, 'utf8')) @@ -908,34 +905,20 @@ describe('PluginPresenter', () => { expect(combined).toContain('get_window_state') expect(combined).toContain('check_permissions') expect(combined).toContain('set_agent_cursor_style') - expect(combined).toContain('DeepChat Computer Use.app') - expect(combined).toContain('AXManualAccessibility') - expect(combined).toContain('electron_debugging_port: 9222') - expect(combined).toContain('screenshot({ window_id })') + expect(combined).toContain('CuaDriver.app') + expect(combined).toContain('win32/x64') + expect(combined).toContain('linux/x64') + expect(combined).toContain('win32/arm64') + expect(combined).toContain('start_recording') + expect(combined).toContain('stop_recording') + expect(combined).not.toContain('screenshot({ window_id })') + expect(combined).not.toContain('set_recording') expect(combined).toContain('zoom({ pid, window_id') expect(combined).toContain('Repeated zoom calls are a failure signal') + expect(combined).toContain('Do not ask the user to install CUA manually') expect(combined).not.toContain('Bash') expect(combined).not.toContain('cua-driver { - const clickTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/ClickTool.swift', - 'utf8' - ) - const rightClickTool = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverServer/Tools/RightClickTool.swift', - 'utf8' - ) - - for (const source of [clickTool, rightClickTool]) { - expect(source).toContain('CUA_DRIVER_MCP_MODE') - expect(source).toContain('Call get_window_state with the same pid and window_id') - } }) it('pins the Feishu MCP bootstrap package and keeps registry selection explicit', async () => { @@ -979,18 +962,7 @@ describe('PluginPresenter', () => { expect(skill).toContain('Feishu plugin settings') }) - it('skips install telemetry in the bundled CUA CLI entrypoint', async () => { - const source = await readFile( - 'plugins/cua/vendor/cua-driver/source/Sources/CuaDriverCLI/CuaDriverCommand.swift', - 'utf8' - ) - - expect(source).not.toContain('recordInstallation()') - expect(source).toContain('telemetryEntryEvent(for: original)') - expect(source).toContain('TelemetryClient.shared.record(event: entryEvent)') - }) - - it('wires CUA plugin packaging docs and release gates for both mac architectures', async () => { + it('wires CUA plugin packaging docs and release gates for supported targets', async () => { const packageJson = JSON.parse(await readFile('package.json', 'utf8')) const buildWorkflow = await readFile('.github/workflows/build.yml', 'utf8') const releaseWorkflow = await readFile('.github/workflows/release.yml', 'utf8') @@ -999,27 +971,58 @@ describe('PluginPresenter', () => { expect(packageJson.scripts['plugin:cua:build:mac:arm64']).toContain('--arch arm64') expect(packageJson.scripts['plugin:cua:build:mac:x64']).toContain('--arch x64') + expect(packageJson.scripts['plugin:cua:build:win:x64']).toContain('--platform win32 --arch x64') + expect(packageJson.scripts['plugin:cua:build:win:arm64']).toContain( + '--platform win32 --arch arm64' + ) + expect(packageJson.scripts['plugin:cua:build:linux:x64']).toContain( + '--platform linux --arch x64' + ) expect(packageJson.scripts['build:mac:arm64']).toContain( 'plugin:bundle -- --name cua --platform darwin --arch arm64' ) expect(packageJson.scripts['build:mac:x64']).toContain( 'plugin:bundle -- --name cua --platform darwin --arch x64' ) - expect(buildWorkflow).toContain('pnpm run plugin:cua:build:mac:${{ matrix.arch }}') + expect(packageJson.scripts['build:win:x64']).toContain( + 'plugin:bundle -- --name cua --platform win32 --arch x64' + ) + expect(packageJson.scripts['build:win:arm64']).toContain( + 'plugin:bundle -- --name cua --platform win32 --arch arm64' + ) + expect(packageJson.scripts['build:linux:x64']).toContain( + 'plugin:bundle -- --name cua --platform linux --arch x64' + ) expect(buildWorkflow).toContain( 'pnpm run plugin:bundle -- --name cua --platform darwin --arch ${{ matrix.arch }}' ) + expect(buildWorkflow).toContain( + 'pnpm run plugin:bundle -- --name cua --platform win32 --arch ${{ matrix.arch }}' + ) + expect(buildWorkflow).toContain( + 'pnpm run plugin:bundle -- --name cua --platform linux --arch ${{ matrix.arch }}' + ) + expect(buildWorkflow).not.toContain('if ("${{ matrix.arch }}" -eq "x64")') expect(buildWorkflow).toContain('Verify bundled plugins') expect(buildWorkflow).toContain('Contents/Resources/app.asar.unpacked/plugins') - expect(releaseWorkflow).toContain('pnpm run plugin:cua:build:mac:${{ matrix.arch }}') expect(releaseWorkflow).toContain( 'pnpm run plugin:bundle -- --name cua --platform darwin --arch ${{ matrix.arch }}' ) + expect(releaseWorkflow).toContain( + 'pnpm run plugin:bundle -- --name cua --platform win32 --arch ${{ matrix.arch }}' + ) + expect(releaseWorkflow).toContain( + 'pnpm run plugin:bundle -- --name cua --platform linux --arch ${{ matrix.arch }}' + ) expect(releaseWorkflow).not.toContain('require_cua_plugin_asset') expect(releaseWorkflow).not.toContain('cp "${dir}/${asset}" release_assets/') expect(packageScript).toContain("parts[0] === 'runtime'") + expect(packageScript).toContain('parts[1] !== args.targetPlatform') expect(packageScript).toContain('parts[2] !== args.targetArch') + expect(packageScript).toContain('CUA plugin does not support') expect(guide).toContain('build/bundled-plugins/') expect(guide).toContain('app.asar.unpacked/plugins/') + expect(guide).toContain('win32/arm64') + expect(guide).toContain('linux/arm64') }) }) From 66ff44a06334f844693f60a9990a9f37ace3f70a Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 00:56:08 +0800 Subject: [PATCH 03/10] fix(cua): address review feedback --- docs/guides/plugin-packaging.md | 2 +- scripts/build-cua-plugin-runtime.mjs | 27 +++++++++++++++------ scripts/package-plugin.mjs | 9 +++++-- scripts/plugin.mjs | 9 +++++-- test/main/presenter/pluginPresenter.test.ts | 8 +++--- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/docs/guides/plugin-packaging.md b/docs/guides/plugin-packaging.md index 6f808942c..e37992626 100644 --- a/docs/guides/plugin-packaging.md +++ b/docs/guides/plugin-packaging.md @@ -177,7 +177,7 @@ uploading artifacts. The release workflow (`.github/workflows/release.yml`) repeats the same steps. Final release uploads app artifacts only; `.dcplugin` files are not published as separate GitHub Release assets. -Expected embedded files (macOS example): +Expected embedded files across platform-specific app packages: ```text app.asar.unpacked/plugins/deepchat-plugin-cua--darwin-x64.dcplugin diff --git a/scripts/build-cua-plugin-runtime.mjs b/scripts/build-cua-plugin-runtime.mjs index 1ada4ad27..1f03c5e63 100644 --- a/scripts/build-cua-plugin-runtime.mjs +++ b/scripts/build-cua-plugin-runtime.mjs @@ -153,13 +153,18 @@ async function downloadFile(url, outputPath) { return } - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`) + try { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`) + } + const buffer = Buffer.from(await response.arrayBuffer()) + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + await fs.writeFile(outputPath, buffer) + } catch (error) { + await fs.rm(outputPath, { force: true }) + throw error } - const buffer = Buffer.from(await response.arrayBuffer()) - await fs.mkdir(path.dirname(outputPath), { recursive: true }) - await fs.writeFile(outputPath, buffer) } async function sha256File(filePath) { @@ -183,10 +188,12 @@ async function verifyChecksum(checksumsPath, assetPath, assetName) { const checksums = parseChecksums(await fs.readFile(checksumsPath, 'utf8')) const expected = checksums.get(assetName) if (!expected) { + await fs.rm(checksumsPath, { force: true }) throw new Error(`checksums.txt does not contain ${assetName}`) } const actual = await sha256File(assetPath) if (actual !== expected) { + await fs.rm(assetPath, { force: true }) throw new Error(`Checksum mismatch for ${assetName}. Expected ${expected}, got ${actual}`) } } @@ -383,8 +390,12 @@ async function signDarwinHelper(runtimeDir) { async function main() { const args = parseArgs(process.argv.slice(2)) - const targetPlatform = args.get('platform') ?? process.env.TARGET_PLATFORM ?? process.platform - const targetArch = args.get('arch') ?? process.env.TARGET_ARCH ?? process.arch + const targetPlatform = String( + args.get('platform') ?? process.env.TARGET_PLATFORM ?? process.platform + ).toLowerCase() + const targetArch = String( + args.get('arch') ?? process.env.TARGET_ARCH ?? process.arch + ).toLowerCase() const metadata = await readUpstreamMetadata() const target = getTarget(targetPlatform, targetArch, metadata) const cacheDir = process.env.DEEPCHAT_CUA_DOWNLOAD_CACHE diff --git a/scripts/package-plugin.mjs b/scripts/package-plugin.mjs index 2f1ddd60e..2263d9b1e 100644 --- a/scripts/package-plugin.mjs +++ b/scripts/package-plugin.mjs @@ -59,6 +59,8 @@ function parseArgs(argv) { if (!args.pluginDir) { throw new Error('Usage: node scripts/package-plugin.mjs [--validate] [--out ] ') } + args.targetPlatform = String(args.targetPlatform).toLowerCase() + args.targetArch = String(args.targetArch).toLowerCase() return args } @@ -218,11 +220,14 @@ function targetKey(targetPlatform, targetArch) { } function isManifestTargetSupported(manifest, targetPlatform, targetArch) { - const aliases = targetPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [targetPlatform] + const normalizedPlatform = String(targetPlatform).toLowerCase() + const normalizedArch = String(targetArch).toLowerCase() + const aliases = + normalizedPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [normalizedPlatform] const targets = manifest.engines?.targets ?? [] if (targets.length > 0) { const supportedTargets = targets.map((target) => String(target).toLowerCase()) - return aliases.some((platform) => supportedTargets.includes(`${platform}/${targetArch}`)) + return aliases.some((platform) => supportedTargets.includes(`${platform}/${normalizedArch}`)) } const platforms = new Set( diff --git a/scripts/plugin.mjs b/scripts/plugin.mjs index aebdefa90..0f45e735c 100644 --- a/scripts/plugin.mjs +++ b/scripts/plugin.mjs @@ -38,6 +38,8 @@ function parseArgs(argv) { console.error('Missing required --plugin-root argument for verify') process.exit(1) } + args.platform = String(args.platform).toLowerCase() + args.arch = String(args.arch).toLowerCase() return args } @@ -83,11 +85,14 @@ function discoverOfficialPlugins() { } function isPluginSupported(plugin, targetPlatform, targetArch) { + const normalizedPlatform = String(targetPlatform).toLowerCase() + const normalizedArch = String(targetArch).toLowerCase() const platforms = new Set(plugin.platforms.map((platform) => String(platform).toLowerCase())) - const aliases = targetPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [targetPlatform] + const aliases = + normalizedPlatform === 'darwin' ? ['darwin', 'macos', 'mac'] : [normalizedPlatform] const targets = plugin.targets.map((target) => String(target).toLowerCase()) if (targets.length > 0) { - return aliases.some((platform) => targets.includes(`${platform}/${targetArch}`)) + return aliases.some((platform) => targets.includes(`${platform}/${normalizedArch}`)) } return aliases.some((platform) => platforms.has(platform)) } diff --git a/test/main/presenter/pluginPresenter.test.ts b/test/main/presenter/pluginPresenter.test.ts index 3ed27a032..53c6d960f 100644 --- a/test/main/presenter/pluginPresenter.test.ts +++ b/test/main/presenter/pluginPresenter.test.ts @@ -793,7 +793,7 @@ describe('PluginPresenter', () => { const manifestTools = manifest.toolPolicies.find( (item: { serverId: string }) => item.serverId === 'cua-driver' ).tools - const expectedAllow = [ + const EXPECTED_ALLOW = [ 'check_permissions', 'list_apps', 'list_windows', @@ -809,7 +809,7 @@ describe('PluginPresenter', () => { 'start_session', 'end_session' ] - const expectedAsk = [ + const EXPECTED_ASK = [ 'launch_app', 'kill_app', 'bring_to_front', @@ -840,11 +840,11 @@ describe('PluginPresenter', () => { 'page' ] - for (const tool of expectedAllow) { + for (const tool of EXPECTED_ALLOW) { expect(manifestTools[tool]).toBe('allow') expect(policy.tools[tool]).toBe('allow') } - for (const tool of expectedAsk) { + for (const tool of EXPECTED_ASK) { expect(manifestTools[tool]).toBe('ask') expect(policy.tools[tool]).toBe('ask') } From d3329dc1a45d4bc786d54a978eace59491febda8 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 01:43:57 +0800 Subject: [PATCH 04/10] fix(ci): stabilize platform build jobs --- .github/workflows/build.yml | 1 + .../cua-cross-platform-computer-use/plan.md | 3 +- .../cua-cross-platform-computer-use/spec.md | 2 +- .../cua-cross-platform-computer-use/tasks.md | 4 +- .../build-action-platform-failures/plan.md | 19 +++++ .../build-action-platform-failures/spec.md | 38 +++++++++ .../build-action-platform-failures/tasks.md | 9 ++ scripts/build-cua-plugin-runtime.mjs | 23 ++++-- scripts/fetch-acp-registry.mjs | 82 ++++++++++++++----- test/main/presenter/pluginPresenter.test.ts | 14 ++++ 10 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 docs/issues/build-action-platform-failures/plan.md create mode 100644 docs/issues/build-action-platform-failures/spec.md create mode 100644 docs/issues/build-action-platform-failures/tasks.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 495bb364f..744c7803e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,6 +77,7 @@ jobs: GITHUB_TOKEN: ${{ env.RTK_INSTALL_GITHUB_TOKEN }} - name: Build Windows + shell: bash run: | pnpm run build pnpm run plugin:bundle -- --name cua --platform win32 --arch ${{ matrix.arch }} diff --git a/docs/features/cua-cross-platform-computer-use/plan.md b/docs/features/cua-cross-platform-computer-use/plan.md index c1a6e8e05..3fe385851 100644 --- a/docs/features/cua-cross-platform-computer-use/plan.md +++ b/docs/features/cua-cross-platform-computer-use/plan.md @@ -56,7 +56,8 @@ pipeline: 6. Validate the extracted layout. 7. Copy the normalized runtime files into `plugins/cua/runtime//`. 8. Set executable permissions for macOS and Linux. -9. Run host-executable smoke checks where possible. +9. Run host-executable smoke checks where the host platform and runtime loader can execute the + target binary. 10. Run macOS app bundle and signing checks for darwin targets. The script should reject Linux arm64 and any other unsupported target with a clear message before diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index 3cff4c163..11545de76 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -113,7 +113,7 @@ Every staged asset must be validated before packaging: - Validate required files exist after extraction. - Normalize executable permissions on POSIX targets. - Validate the driver can be executed for a low-risk command such as `--version` when the host - platform can run the target binary. + platform and runtime loader can run the target binary. - Keep macOS signing and helper-app validation in place where a `.app` bundle is staged. ## Runtime Layout diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md index c47d27632..18e00c5ee 100644 --- a/docs/features/cua-cross-platform-computer-use/tasks.md +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -18,7 +18,7 @@ - Validate macOS helper app executable path and signing state. - Validate Windows `cua-driver.exe` plus `cua-driver-uia.exe`. - Validate Linux `cua-driver` and executable permissions. - - Add host-compatible `--version` smoke checks. + - Add host-compatible `--version` smoke checks with a loader-version guard. - [x] T04 - Update CUA plugin manifest - Expand support from macOS-only to the supported target matrix. @@ -81,6 +81,8 @@ - Bundle and verify the Windows x64 CUA plugin on the current Windows host. - Bundle and verify the Windows arm64 CUA plugin without running the non-host binary. - Inspect the generated `.dcplugin` archive contents. + - On Linux hosts with an older glibc loader than the pinned upstream binary requires, verify that + staging still validates checksum, layout, file presence, and executable permissions. - [ ] T14 - Verify CI-only targets - Use CI to validate macOS arm64/x64 packaging and signing. diff --git a/docs/issues/build-action-platform-failures/plan.md b/docs/issues/build-action-platform-failures/plan.md new file mode 100644 index 000000000..56baef294 --- /dev/null +++ b/docs/issues/build-action-platform-failures/plan.md @@ -0,0 +1,19 @@ +# Build Action Platform Failures Plan + +## Changes + +- Update the ACP registry fetcher to use Node's `https` module and sequential icon downloads so the + Windows arm64 build avoids the current built-in fetch crash path. +- Add a Linux glibc loader-mismatch branch to the CUA runtime smoke check. The staging script keeps + all file validation and skips only the host execution check when the runner cannot load the + target binary. +- Run the Windows build step under bash so each command exits immediately on failure. +- Add focused textual regression checks for the build scripts and workflow. + +## Verification + +- `pnpm run format` +- `pnpm run i18n` +- `pnpm run lint` +- `pnpm test -- test/main/presenter/pluginPresenter.test.ts` +- `pnpm run plugin:bundle -- --name cua --platform linux --arch x64` diff --git a/docs/issues/build-action-platform-failures/spec.md b/docs/issues/build-action-platform-failures/spec.md new file mode 100644 index 000000000..2e47f048a --- /dev/null +++ b/docs/issues/build-action-platform-failures/spec.md @@ -0,0 +1,38 @@ +# Build Action Platform Failures Spec + +## Status + +In progress. + +## Goal + +Restore the manual Build Application workflow for the current CUA cross-platform branch so the +Windows arm64 and Linux x64 jobs can produce packaged artifacts for maintainer verification. + +## Background + +GitHub Actions run `27634409921` failed on two jobs: + +- `build-windows(arm64)` crashed during `scripts/fetch-acp-registry.mjs` before `pnpm run build` + produced `out/main/index.js`; the PowerShell multi-line step then continued into packaging and + reported a secondary missing-entry asar error. +- `build-linux (x64)` staged the CUA runtime but failed the executable smoke check because the + upstream Linux binary requires `GLIBC_2.39`, while the Ubuntu 22.04 runner provides an older + loader. + +## Acceptance Criteria + +- Windows arm64 build steps stop at the first failing command and surface the real failure. +- `scripts/fetch-acp-registry.mjs` avoids the Windows arm64 registry-fetch crash path while still + refreshing the registry and cached icons. +- Linux x64 CUA runtime staging still validates checksum, archive layout, executable presence, and + permissions. +- Linux x64 CUA runtime smoke checks run when the host loader can execute the binary and skip only + for a detected glibc loader-version mismatch. +- The Build Application workflow can be pushed again for maintainer validation. + +## Non-Goals + +- Change the pinned CUA upstream release. +- Change the packaged Linux runner baseline. +- Redesign ACP registry runtime loading. diff --git a/docs/issues/build-action-platform-failures/tasks.md b/docs/issues/build-action-platform-failures/tasks.md new file mode 100644 index 000000000..4c6967dcd --- /dev/null +++ b/docs/issues/build-action-platform-failures/tasks.md @@ -0,0 +1,9 @@ +# Build Action Platform Failures Tasks + +- [x] Inspect failed GitHub Actions logs. +- [x] Update ACP registry build-time fetch behavior. +- [x] Update CUA Linux smoke-check behavior. +- [x] Make Windows build workflow fail fast. +- [x] Add focused regression coverage. +- [x] Run local verification. +- [ ] Push the branch to trigger a new Build Application workflow. diff --git a/scripts/build-cua-plugin-runtime.mjs b/scripts/build-cua-plugin-runtime.mjs index 1f03c5e63..f9fad697d 100644 --- a/scripts/build-cua-plugin-runtime.mjs +++ b/scripts/build-cua-plugin-runtime.mjs @@ -325,6 +325,10 @@ function canRunTarget(targetPlatform, targetArch) { return process.platform === targetPlatform && process.arch === targetArch } +function isLinuxGlibcLoaderMismatch(output) { + return /libc\.so\.6/.test(output) && /GLIBC_\d+\.\d+/.test(output) && /not found/.test(output) +} + function smokeCheck(executable, targetPlatform, targetArch) { if (!canRunTarget(targetPlatform, targetArch)) { console.log(`Skipping CUA runtime smoke check for non-host target ${targetPlatform}/${targetArch}`) @@ -340,6 +344,13 @@ function smokeCheck(executable, targetPlatform, targetArch) { throw result.error } if (result.status !== 0) { + const output = `${result.stderr || ''}${result.stdout || ''}` + if (targetPlatform === 'linux' && isLinuxGlibcLoaderMismatch(output)) { + console.warn( + `Skipping CUA runtime smoke check because the host glibc loader cannot execute ${targetPlatform}/${targetArch}: ${output.trim()}` + ) + return + } throw new Error( `CUA runtime smoke check failed with exit code ${result.status}: ${result.stderr || result.stdout}` ) @@ -347,8 +358,8 @@ function smokeCheck(executable, targetPlatform, targetArch) { console.log((result.stdout || result.stderr).trim()) } -function validateDarwinArchitecture(executable, targetArch) { - if (process.platform !== 'darwin') { +function validateDarwinArchitecture(executable, targetPlatform, targetArch) { + if (targetPlatform !== 'darwin' || process.platform !== 'darwin') { return } ensureTool('/usr/bin/lipo', ['-info', process.execPath]) @@ -359,8 +370,8 @@ function validateDarwinArchitecture(executable, targetArch) { } } -async function signDarwinHelper(runtimeDir) { - if (process.platform !== 'darwin') { +async function signDarwinHelper(runtimeDir, targetPlatform) { + if (targetPlatform !== 'darwin' || process.platform !== 'darwin') { return } ensureTool('codesign', ['--version']) @@ -416,8 +427,8 @@ async function main() { await extractArchive(assetPath, extractDir) const { runtimeDir, executable } = await stageRuntime(targetPlatform, targetArch, extractDir) - validateDarwinArchitecture(executable, targetArch) - await signDarwinHelper(runtimeDir) + validateDarwinArchitecture(executable, targetPlatform, targetArch) + await signDarwinHelper(runtimeDir, targetPlatform) smokeCheck(executable, targetPlatform, targetArch) const relativeRuntimePath = path.relative(rootDir, runtimeDir) diff --git a/scripts/fetch-acp-registry.mjs b/scripts/fetch-acp-registry.mjs index f13d8fabf..ac9ea9547 100644 --- a/scripts/fetch-acp-registry.mjs +++ b/scripts/fetch-acp-registry.mjs @@ -1,5 +1,7 @@ -import fs from 'fs/promises' -import path from 'path' +import fs from 'node:fs/promises' +import { request } from 'node:https' +import path from 'node:path' +import { URL } from 'node:url' const REGISTRY_URL = 'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json' const OUTPUT_DIR = path.resolve(process.cwd(), 'resources', 'acp-registry') @@ -8,6 +10,57 @@ const ICON_OUTPUT_DIR = path.join(OUTPUT_DIR, 'icons') const ICON_TMP_DIR = path.join(OUTPUT_DIR, '.icons-tmp') const ACP_REGISTRY_ICON_PREFIX = 'https://cdn.agentclientprotocol.com/registry/' const SAFE_ICON_ID_PATTERN = /^[A-Za-z0-9._-]+$/ +const REQUEST_TIMEOUT_MS = 30_000 +const MAX_REDIRECTS = 5 +const USER_AGENT = 'DeepChat build registry fetcher' + +const fetchText = (url, redirectCount = 0) => + new Promise((resolve, reject) => { + const parsedUrl = new URL(url) + const req = request( + parsedUrl, + { + headers: { + accept: 'application/json,image/svg+xml,text/plain,*/*', + 'user-agent': USER_AGENT + } + }, + (response) => { + const statusCode = response.statusCode ?? 0 + const location = response.headers.location + if (statusCode >= 300 && statusCode < 400 && location) { + response.resume() + if (redirectCount >= MAX_REDIRECTS) { + reject(new Error(`Too many redirects while fetching ${url}`)) + return + } + resolve(fetchText(new URL(location, parsedUrl).toString(), redirectCount + 1)) + return + } + + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.on('end', () => { + const text = Buffer.concat(chunks).toString('utf-8') + if (statusCode < 200 || statusCode >= 300) { + reject( + new Error( + `Failed to fetch ${url}: ${statusCode} ${response.statusMessage ?? ''}`.trim() + ) + ) + return + } + resolve(text) + }) + } + ) + + req.setTimeout(REQUEST_TIMEOUT_MS, () => { + req.destroy(new Error(`Timed out fetching ${url}`)) + }) + req.on('error', reject) + req.end() + }) const getCacheableIconAgents = (parsed) => Array.isArray(parsed.agents) @@ -51,18 +104,11 @@ const stageIcons = async (parsed) => { await fs.rm(ICON_TMP_DIR, { recursive: true, force: true }) await fs.mkdir(ICON_TMP_DIR, { recursive: true }) - await Promise.all( - iconAgents.map(async (agent) => { - const safeAgentId = sanitizeAgentId(agent.id) - const response = await fetch(agent.icon) - if (!response.ok) { - throw new Error(`Failed to fetch ACP icon ${agent.id}: ${response.status} ${response.statusText}`) - } - - const text = await response.text() - await fs.writeFile(path.join(ICON_TMP_DIR, `${safeAgentId}.svg`), text, 'utf-8') - }) - ) + for (const agent of iconAgents) { + const safeAgentId = sanitizeAgentId(agent.id) + const text = await fetchText(agent.icon) + await fs.writeFile(path.join(ICON_TMP_DIR, `${safeAgentId}.svg`), text, 'utf-8') + } return iconAgents.length } @@ -73,12 +119,8 @@ const commitStagedIcons = async () => { } const main = async () => { - const response = await fetch(REGISTRY_URL) - if (!response.ok) { - throw new Error(`Failed to fetch ACP registry: ${response.status} ${response.statusText}`) - } - - const text = await response.text() + console.log(`[fetch-acp-registry] Fetching ${REGISTRY_URL}`) + const text = await fetchText(REGISTRY_URL) const parsed = JSON.parse(text) const iconCount = await stageIcons(parsed) diff --git a/test/main/presenter/pluginPresenter.test.ts b/test/main/presenter/pluginPresenter.test.ts index 53c6d960f..a3edbada1 100644 --- a/test/main/presenter/pluginPresenter.test.ts +++ b/test/main/presenter/pluginPresenter.test.ts @@ -876,12 +876,25 @@ describe('PluginPresenter', () => { expect(metadata.assets['linux-x64'].name).toBe('cua-driver-rs-0.5.5-linux-x86_64-binary.tar.gz') expect(buildScript).toContain('verifyChecksum') expect(buildScript).toContain('downloadFile') + expect(buildScript).toContain('isLinuxGlibcLoaderMismatch') + expect(buildScript).toContain('host glibc loader') + expect(buildScript).toContain("targetPlatform !== 'darwin'") + expect(buildScript).toContain('signDarwinHelper(runtimeDir, targetPlatform)') expect(buildScript).toContain('sourceKind') expect(buildScript).toContain('upstream-release') expect(buildScript).not.toContain('swift') expect(buildScript).not.toContain('--package-path') }) + it('keeps ACP registry build-time fetching compatible with Windows arm64', async () => { + const source = await readFile('scripts/fetch-acp-registry.mjs', 'utf8') + + expect(source).toContain('node:https') + expect(source).toContain('for (const agent of iconAgents)') + expect(source).not.toContain('Promise.all(') + expect(source).not.toContain('fetch(') + }) + it('keeps unreviewed CUA tools out of the policy surface', async () => { const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) const policy = JSON.parse(await readFile('plugins/cua/policies/tool-policy.json', 'utf8')) @@ -1002,6 +1015,7 @@ describe('PluginPresenter', () => { expect(buildWorkflow).toContain( 'pnpm run plugin:bundle -- --name cua --platform linux --arch ${{ matrix.arch }}' ) + expect(buildWorkflow).toContain('- name: Build Windows\n shell: bash') expect(buildWorkflow).not.toContain('if ("${{ matrix.arch }}" -eq "x64")') expect(buildWorkflow).toContain('Verify bundled plugins') expect(buildWorkflow).toContain('Contents/Resources/app.asar.unpacked/plugins') From e7e699fc060f778eee4c7878acc1f2b7e25ced40 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 08:53:00 +0800 Subject: [PATCH 05/10] fix(ci): add draggable type shim --- docs/issues/build-action-platform-failures/plan.md | 2 ++ docs/issues/build-action-platform-failures/spec.md | 4 ++++ docs/issues/build-action-platform-failures/tasks.md | 3 ++- src/renderer/src/types/vuedraggable.d.ts | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/types/vuedraggable.d.ts diff --git a/docs/issues/build-action-platform-failures/plan.md b/docs/issues/build-action-platform-failures/plan.md index 56baef294..17cb7b629 100644 --- a/docs/issues/build-action-platform-failures/plan.md +++ b/docs/issues/build-action-platform-failures/plan.md @@ -8,6 +8,8 @@ all file validation and skips only the host execution check when the runner cannot load the target binary. - Run the Windows build step under bash so each command exits immediately on failure. +- Add a local `vuedraggable` module declaration so `vue-tsgo` has a stable type source on macOS + arm64 runners. - Add focused textual regression checks for the build scripts and workflow. ## Verification diff --git a/docs/issues/build-action-platform-failures/spec.md b/docs/issues/build-action-platform-failures/spec.md index 2e47f048a..1b38c033a 100644 --- a/docs/issues/build-action-platform-failures/spec.md +++ b/docs/issues/build-action-platform-failures/spec.md @@ -19,6 +19,9 @@ GitHub Actions run `27634409921` failed on two jobs: - `build-linux (x64)` staged the CUA runtime but failed the executable smoke check because the upstream Linux binary requires `GLIBC_2.39`, while the Ubuntu 22.04 runner provides an older loader. +- The follow-up run `27636722380` passed Windows x64, Windows arm64, and Linux x64, then failed + `build-mac (arm64)` during `vue-tsgo` because `vuedraggable` module typing was not resolved on + that runner. ## Acceptance Criteria @@ -29,6 +32,7 @@ GitHub Actions run `27634409921` failed on two jobs: permissions. - Linux x64 CUA runtime smoke checks run when the host loader can execute the binary and skip only for a detected glibc loader-version mismatch. +- macOS arm64 typecheck resolves `vuedraggable` for the existing draggable list components. - The Build Application workflow can be pushed again for maintainer validation. ## Non-Goals diff --git a/docs/issues/build-action-platform-failures/tasks.md b/docs/issues/build-action-platform-failures/tasks.md index 4c6967dcd..e5c339902 100644 --- a/docs/issues/build-action-platform-failures/tasks.md +++ b/docs/issues/build-action-platform-failures/tasks.md @@ -6,4 +6,5 @@ - [x] Make Windows build workflow fail fast. - [x] Add focused regression coverage. - [x] Run local verification. -- [ ] Push the branch to trigger a new Build Application workflow. +- [x] Add stable `vuedraggable` type resolution for macOS arm64 CI. +- [x] Push the branch to trigger a new Build Application workflow. diff --git a/src/renderer/src/types/vuedraggable.d.ts b/src/renderer/src/types/vuedraggable.d.ts new file mode 100644 index 000000000..8d3cbc723 --- /dev/null +++ b/src/renderer/src/types/vuedraggable.d.ts @@ -0,0 +1,6 @@ +import type { DefineComponent } from 'vue' + +declare module 'vuedraggable' { + const draggable: DefineComponent + export default draggable +} From 3aa9332600e559ebd169e8b64a4c956fd0b37d72 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 09:18:23 +0800 Subject: [PATCH 06/10] fix(cua): expose plugin settings targets --- .../cua-cross-platform-computer-use/spec.md | 2 + src/main/presenter/windowPresenter/index.ts | 7 ++- src/preload/index.d.ts | 1 + src/preload/index.ts | 1 + src/renderer/api/runtime.ts | 4 ++ src/renderer/settings/App.vue | 12 +++-- .../settings/components/SettingsOverview.vue | 10 ++-- src/renderer/settings/main.ts | 6 ++- src/shared/settingsNavigation.ts | 47 ++++++++++++++----- test/main/shared/settingsNavigation.test.ts | 38 ++++++++++++--- 10 files changed, 99 insertions(+), 29 deletions(-) diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index 11545de76..8cbda8cbc 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -211,6 +211,8 @@ The packaged app must keep CUA usable after Electron packaging: - Direct CUA runtime packaging for Linux arm64 fails with a clear unsupported-target message. - Official plugin visibility is gated by platform and arch, so the unsupported Linux arm target does not show CUA as available. +- The settings sidebar and settings routes expose the Plugins entry on supported CUA targets, not + only on macOS, while keeping unsupported CUA targets hidden. - Runtime detection resolves the plugin-local binary on every supported target. - The plugin starts through DeepChat's internal tool path without user-managed MCP setup. - Skill docs describe DeepChat usage and platform caveats, not upstream manual installer workflows. diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index 402dd37fe..483c91fcd 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -1627,7 +1627,12 @@ export class WindowPresenter implements IWindowPresenter { private getSettingsWindowTargetUrl(navigation?: SettingsNavigationPayload): string { const initialNavigationPath = navigation - ? resolveSettingsNavigationPath(navigation.routeName, navigation.params, process.platform) + ? resolveSettingsNavigationPath( + navigation.routeName, + navigation.params, + process.platform, + process.arch + ) : null if (is.dev && process.env['ELECTRON_RENDERER_URL']) { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index ffbcb9592..0df5901d5 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -9,6 +9,7 @@ declare global { readClipboardText(): string getPathForFile(file: File): string getPlatform(): string + getArch(): string openExternal?(url: string): Promise toRelativePath?(filePath: string, baseDir?: string): string formatPathForInput?(filePath: string): string diff --git a/src/preload/index.ts b/src/preload/index.ts index cb5ea169b..d9975ec3e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -31,6 +31,7 @@ const api = Object.freeze({ return webUtils.getPathForFile(file) }, getPlatform: () => process.platform, + getArch: () => process.arch, openExternal: (url: string) => { const externalUrl = normalizeExternalUrl(url) if (!externalUrl) { diff --git a/src/renderer/api/runtime.ts b/src/renderer/api/runtime.ts index 2fedaba19..597a6b534 100644 --- a/src/renderer/api/runtime.ts +++ b/src/renderer/api/runtime.ts @@ -63,6 +63,10 @@ export function getRuntimePlatform(): string | undefined { return getRendererRuntimeApi().getPlatform?.() } +export function getRuntimeArch(): string | undefined { + return getRendererRuntimeApi().getArch?.() +} + export async function openRuntimeExternal(url: string): Promise { const runtimeApi = getRendererRuntimeApi() if (!runtimeApi.openExternal) { diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index d58724183..94c64a9ca 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -85,7 +85,7 @@ import { useTitle } from '@vueuse/core' import { createConfigClient } from '@api/ConfigClient' import { createDeviceClient } from '@api/DeviceClient' import { createWindowClient } from '@api/WindowClient' -import { getRuntimePlatform } from '@api/runtime' +import { getRuntimeArch, getRuntimePlatform } from '@api/runtime' import CloseIcon from './icons/CloseIcon.vue' import { useUiSettingsStore } from '../src/stores/uiSettingsStore' import { useLanguageStore } from '../src/stores/language' @@ -452,6 +452,8 @@ const cleanupSettingsProviderInstall = windowClient.onSettingsProviderInstall(() const notifySettingsReady = () => { void windowClient.notifySettingsReady() } +const runtimePlatform = getRuntimePlatform() +const runtimeArch = getRuntimeArch() const settings: Ref< { title: string @@ -460,23 +462,23 @@ const settings: Ref< path: string }[] > = ref( - getSettingsRouteItems(getRuntimePlatform()).map((item) => ({ + getSettingsRouteItems(runtimePlatform, runtimeArch).map((item) => ({ title: item.titleKey, name: item.routeName, icon: item.icon, - path: resolveSettingsNavigationPath(item.routeName) + path: resolveSettingsNavigationPath(item.routeName, undefined, runtimePlatform, runtimeArch) })) ) const settingGroups = ref( - getSettingsNavigationGroups(getRuntimePlatform()).map((group) => ({ + getSettingsNavigationGroups(runtimePlatform, runtimeArch).map((group) => ({ key: group.key, titleKey: group.titleKey, items: group.items.map((item) => ({ title: item.titleKey, name: item.routeName, icon: item.icon, - path: resolveSettingsNavigationPath(item.routeName) + path: resolveSettingsNavigationPath(item.routeName, undefined, runtimePlatform, runtimeArch) })) })) ) diff --git a/src/renderer/settings/components/SettingsOverview.vue b/src/renderer/settings/components/SettingsOverview.vue index 1e1c7745a..6ac4a0035 100644 --- a/src/renderer/settings/components/SettingsOverview.vue +++ b/src/renderer/settings/components/SettingsOverview.vue @@ -183,7 +183,7 @@ import SettingsPageShell from './control-center/SettingsPageShell.vue' import SettingsSectionCard from './control-center/SettingsSectionCard.vue' import StatusMetricCard from './control-center/StatusMetricCard.vue' import DashboardSettings from './DashboardSettings.vue' -import { getRuntimePlatform } from '@api/runtime' +import { getRuntimeArch, getRuntimePlatform } from '@api/runtime' const { t, locale } = useI18n() const router = useRouter() @@ -198,7 +198,9 @@ const agentStore = useAgentStore() const activities = ref([]) const searchQuery = ref('') const usageDashboardRef = ref(null) -const settingsItems = getSettingsNavigationItems(getRuntimePlatform()) +const runtimePlatform = getRuntimePlatform() +const runtimeArch = getRuntimeArch() +const settingsItems = getSettingsNavigationItems(runtimePlatform, runtimeArch) type SettingsRouteName = SettingsNavigationItem['routeName'] const enabledProvidersCount = computed( @@ -282,7 +284,9 @@ const searchResults = computed(() => { }) const openRoute = (routeName: SettingsRouteName) => { - void router.push(resolveSettingsNavigationPath(routeName)) + void router.push( + resolveSettingsNavigationPath(routeName, undefined, runtimePlatform, runtimeArch) + ) } const openActivity = (activity: SettingsActivityRecord) => { diff --git a/src/renderer/settings/main.ts b/src/renderer/settings/main.ts index 6b2a0b4e4..d18a6d261 100644 --- a/src/renderer/settings/main.ts +++ b/src/renderer/settings/main.ts @@ -9,9 +9,11 @@ import { createI18n } from 'vue-i18n' import locales, { pluralRules } from '@/i18n' import { getSettingsRouteItems } from '@shared/settingsNavigation' import { preloadIcons } from '../src/lib/iconLoader' -import { getRuntimePlatform } from '@api/runtime' +import { getRuntimeArch, getRuntimePlatform } from '@api/runtime' -const settingsRouteItems = getSettingsRouteItems(getRuntimePlatform()) +const runtimePlatform = getRuntimePlatform() +const runtimeArch = getRuntimeArch() +const settingsRouteItems = getSettingsRouteItems(runtimePlatform, runtimeArch) const settingsRouteComponents = { 'settings-overview': () => import('./components/SettingsOverview.vue'), diff --git a/src/shared/settingsNavigation.ts b/src/shared/settingsNavigation.ts index 57344e13f..9f848c225 100644 --- a/src/shared/settingsNavigation.ts +++ b/src/shared/settingsNavigation.ts @@ -26,6 +26,7 @@ export interface SettingsNavigationItem { groupKey: SettingsNavigationGroupKey keywords: string[] supportedPlatforms?: string[] + supportedTargets?: string[] hiddenInSidebar?: boolean } @@ -211,7 +212,7 @@ export const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [ position: 5.75, groupKey: 'tools', keywords: ['plugin', 'plugins', 'extension', 'runtime', '插件', '扩展', '运行时'], - supportedPlatforms: ['darwin'] + supportedTargets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64'] }, { routeName: 'settings-skills', @@ -275,10 +276,10 @@ const getPlatformAliases = (platform?: string): Set => { return new Set() } - if (normalized === 'darwin') { + if (['darwin', 'macos', 'mac'].includes(normalized)) { return new Set(['darwin', 'macos', 'mac']) } - if (normalized === 'win32') { + if (['win32', 'windows', 'win'].includes(normalized)) { return new Set(['win32', 'windows', 'win']) } @@ -287,8 +288,21 @@ const getPlatformAliases = (platform?: string): Set => { export const isSettingsNavigationItemSupported = ( item: SettingsNavigationItem, - platform?: string + platform?: string, + arch?: string ): boolean => { + if (item.supportedTargets?.length) { + if (!platform || !arch) { + return true + } + const normalizedArch = arch.trim().toLowerCase() + const aliases = getPlatformAliases(platform) + const targets = item.supportedTargets.map((target) => target.trim().toLowerCase()) + return [...aliases].some((platformAlias) => + targets.includes(`${platformAlias}/${normalizedArch}`) + ) + } + if (!item.supportedPlatforms?.length) { return true } @@ -302,14 +316,22 @@ export const isSettingsNavigationItemSupported = ( ) } -export const getSettingsNavigationItems = (platform?: string): SettingsNavigationItem[] => - getSettingsRouteItems(platform).filter((item) => !item.hiddenInSidebar) +export const getSettingsRouteItems = (platform?: string, arch?: string): SettingsNavigationItem[] => + SETTINGS_NAVIGATION_ITEMS.filter((item) => + isSettingsNavigationItemSupported(item, platform, arch) + ) -export const getSettingsRouteItems = (platform?: string): SettingsNavigationItem[] => - SETTINGS_NAVIGATION_ITEMS.filter((item) => isSettingsNavigationItemSupported(item, platform)) +export const getSettingsNavigationItems = ( + platform?: string, + arch?: string +): SettingsNavigationItem[] => + getSettingsRouteItems(platform, arch).filter((item) => !item.hiddenInSidebar) -export const getSettingsNavigationGroups = (platform?: string): SettingsNavigationGroup[] => { - const items = getSettingsNavigationItems(platform) +export const getSettingsNavigationGroups = ( + platform?: string, + arch?: string +): SettingsNavigationGroup[] => { + const items = getSettingsNavigationItems(platform, arch) return SETTINGS_NAVIGATION_GROUPS.map((group) => ({ ...group, @@ -322,9 +344,10 @@ export const getSettingsNavigationGroups = (platform?: string): SettingsNavigati export const resolveSettingsNavigationPath = ( routeName: SettingsNavigationItem['routeName'], params?: Record, - platform?: string + platform?: string, + arch?: string ): string => { - const item = getSettingsRouteItems(platform).find( + const item = getSettingsRouteItems(platform, arch).find( (navigationItem) => navigationItem.routeName === routeName ) if (!item) { diff --git a/test/main/shared/settingsNavigation.test.ts b/test/main/shared/settingsNavigation.test.ts index a420f328f..40989a202 100644 --- a/test/main/shared/settingsNavigation.test.ts +++ b/test/main/shared/settingsNavigation.test.ts @@ -34,16 +34,42 @@ describe('settings navigation helpers', () => { expect(resolveSettingsNavigationPath('settings-provider')).toBe('/provider') }) - it('hides plugin settings navigation on unsupported platforms', () => { + it('shows plugin settings navigation on CUA-supported targets', () => { expect( - getSettingsNavigationItems('darwin').some((item) => item.routeName === 'settings-plugins') + getSettingsNavigationItems('darwin', 'arm64').some( + (item) => item.routeName === 'settings-plugins' + ) ).toBe(true) expect( - getSettingsNavigationItems('win32').some((item) => item.routeName === 'settings-plugins') - ).toBe(false) + getSettingsNavigationItems('win32', 'x64').some( + (item) => item.routeName === 'settings-plugins' + ) + ).toBe(true) + expect( + getSettingsNavigationItems('windows', 'x64').some( + (item) => item.routeName === 'settings-plugins' + ) + ).toBe(true) + expect( + getSettingsNavigationItems('win32', 'arm64').some( + (item) => item.routeName === 'settings-plugins' + ) + ).toBe(true) expect( - getSettingsNavigationItems('linux').some((item) => item.routeName === 'settings-plugins') + getSettingsNavigationItems('linux', 'x64').some( + (item) => item.routeName === 'settings-plugins' + ) + ).toBe(true) + }) + + it('hides plugin settings navigation on CUA-unsupported targets', () => { + expect( + getSettingsNavigationItems('linux', 'arm64').some( + (item) => item.routeName === 'settings-plugins' + ) ).toBe(false) - expect(resolveSettingsNavigationPath('settings-plugins', undefined, 'win32')).toBe('/overview') + expect(resolveSettingsNavigationPath('settings-plugins', undefined, 'linux', 'arm64')).toBe( + '/overview' + ) }) }) From e3df22d5b99c28bee5e9f8b561e7a08872f22d09 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 10:24:52 +0800 Subject: [PATCH 07/10] fix(cua): scope packaged targets --- .../cua-cross-platform-computer-use/plan.md | 2 + .../cua-cross-platform-computer-use/spec.md | 4 + .../cua-cross-platform-computer-use/tasks.md | 1 + scripts/package-plugin.mjs | 14 +- test/main/presenter/pluginPresenter.test.ts | 78 +++++++++++ test/main/scripts/packagePlugin.test.ts | 128 ++++++++++++++++++ 6 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 test/main/scripts/packagePlugin.test.ts diff --git a/docs/features/cua-cross-platform-computer-use/plan.md b/docs/features/cua-cross-platform-computer-use/plan.md index 3fe385851..b8e4e6676 100644 --- a/docs/features/cua-cross-platform-computer-use/plan.md +++ b/docs/features/cua-cross-platform-computer-use/plan.md @@ -69,6 +69,8 @@ Update `scripts/package-plugin.mjs`: - Remove the darwin-only CUA guard. - Keep only the selected `runtime//` subtree in the `.dcplugin` artifact. +- Narrow the packaged manifest's `engines.targets` to the selected `/` target so + target-specific artifacts cannot be discovered on the wrong architecture. - Validate required files per target: - macOS: helper app executable. - Windows: `cua-driver.exe` and `cua-driver-uia.exe`. diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index 8cbda8cbc..eedab9a51 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -193,6 +193,8 @@ Platform permission behavior must be explicit: The packaged app must keep CUA usable after Electron packaging: - The `.dcplugin` artifact must contain the correct runtime subtree for its platform and arch. +- The packaged `.dcplugin` manifest must narrow `engines.targets` to the artifact's own + platform/arch target, even though the source manifest keeps the full supported target matrix. - Runtime files must stay outside `app.asar`. - Supported Windows archives must include `cua-driver-uia.exe` next to `cua-driver.exe`. - Linux runtime files must retain executable permissions after package extraction. @@ -205,6 +207,8 @@ The packaged app must keep CUA usable after Electron packaging: - Official CUA plugin metadata or discovery logic allows only the supported target matrix: `darwin/arm64`, `darwin/x64`, `win32/x64`, `win32/arm64`, and `linux/x64`. +- Each target-specific CUA `.dcplugin` advertises only its own `engines.targets` entry, so + side-by-side artifacts cannot be selected on the wrong CPU architecture. - Packaged macOS, Windows, and Linux x64 builds include a CUA `.dcplugin` artifact. - Packaged Windows arm64 builds include a CUA `.dcplugin` artifact. - Packaged Linux arm64 builds do not include a visible or usable CUA plugin. diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md index 18e00c5ee..2777f93d8 100644 --- a/docs/features/cua-cross-platform-computer-use/tasks.md +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -37,6 +37,7 @@ - [x] T06 - Update plugin packaging - Remove darwin-only CUA validation in `scripts/package-plugin.mjs`. - Package only the selected `runtime//` subtree. + - Scope packaged target metadata to the selected `runtime//` subtree. - Preserve POSIX executable permissions. - Verify the `.dcplugin` artifact contains the expected files for each supported target. diff --git a/scripts/package-plugin.mjs b/scripts/package-plugin.mjs index 2263d9b1e..3a57f8745 100644 --- a/scripts/package-plugin.mjs +++ b/scripts/package-plugin.mjs @@ -192,6 +192,10 @@ function artifactFileName(manifest, targetPlatform, targetArch) { return `${safeId}-${manifest.version}${targetSuffix}.dcplugin` } +function targetKey(targetPlatform, targetArch) { + return `${targetPlatform}/${targetArch}` +} + function releaseTag(version) { return version.startsWith('v') ? version : `v${version}` } @@ -208,6 +212,12 @@ function createPackageManifest(manifest, args) { `https://github.com/ThinkInAIXYZ/deepchat/releases/download/${releaseTag(version)}` ) ) + if ( + Array.isArray(next.engines?.targets) && + isManifestTargetSupported(next, args.targetPlatform, args.targetArch) + ) { + next.engines.targets = [targetKey(args.targetPlatform, args.targetArch)] + } if (next.source?.type === OFFICIAL_PLUGIN_SOURCE) { const assetName = artifactFileName(next, args.targetPlatform, args.targetArch) next.source.url = `https://github.com/ThinkInAIXYZ/deepchat/releases/download/${releaseTag(version)}/${assetName}` @@ -215,10 +225,6 @@ function createPackageManifest(manifest, args) { return next } -function targetKey(targetPlatform, targetArch) { - return `${targetPlatform}/${targetArch}` -} - function isManifestTargetSupported(manifest, targetPlatform, targetArch) { const normalizedPlatform = String(targetPlatform).toLowerCase() const normalizedArch = String(targetArch).toLowerCase() diff --git a/test/main/presenter/pluginPresenter.test.ts b/test/main/presenter/pluginPresenter.test.ts index a3edbada1..d21590aa5 100644 --- a/test/main/presenter/pluginPresenter.test.ts +++ b/test/main/presenter/pluginPresenter.test.ts @@ -205,6 +205,46 @@ const createBundledFixture = async ( } } +const createOfficialPackage = async (options: { + packageRoot: string + packagePath: string + pluginId: string + name: string + targets: string[] +}) => { + const manifest = { + id: options.pluginId, + name: options.name, + version: '0.2.3', + publisher: 'DeepChat', + engines: { + deepchat: '>=0.2.3', + platforms: ['win32'], + targets: options.targets + }, + activationEvents: ['onEnable'], + capabilities: [], + source: { + type: 'deepchat-official', + url: `https://github.com/ThinkInAIXYZ/deepchat/releases/download/v0.2.3/${path.basename(options.packagePath)}`, + publisher: 'DeepChat' + } + } + const files: Record = { + 'plugin.json': new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`) + } + const checksums = Object.fromEntries( + Object.entries(files).map(([filePath, content]) => [ + filePath, + createHash('sha256').update(Buffer.from(content)).digest('hex') + ]) + ) + files['checksums.json'] = new TextEncoder().encode(`${JSON.stringify(checksums, null, 2)}\n`) + + await mkdir(options.packageRoot, { recursive: true }) + await writeFile(options.packagePath, Buffer.from(zipSync(files, { level: 6 }))) +} + const createDirectoryFixture = async ( options: { appPath?: string @@ -361,6 +401,44 @@ describe('PluginPresenter', () => { ) }) + it('selects the matching CUA package when target artifacts are side by side', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'deepchat-cua-package-target-test-')) + tempRoots.push(root) + const appPath = path.join(root, 'app') + const userDataPath = path.join(root, 'userData') + const packageRoot = path.join(root, 'build', 'bundled-plugins') + const pluginId = 'com.deepchat.plugins.cua' + const winX64Package = path.join(packageRoot, 'deepchat-plugin-cua-0.2.3-win32-x64.dcplugin') + const winArmPackage = path.join(packageRoot, 'deepchat-plugin-cua-0.2.3-win32-arm64.dcplugin') + await mkdir(userDataPath, { recursive: true }) + await createOfficialPackage({ + packageRoot, + packagePath: winArmPackage, + pluginId, + name: 'CUA Windows ARM64', + targets: ['win32/arm64'] + }) + await createOfficialPackage({ + packageRoot, + packagePath: winX64Package, + pluginId, + name: 'CUA Windows X64', + targets: ['win32/x64'] + }) + vi.mocked(app.getPath).mockImplementation((name: string) => + name === 'userData' ? userDataPath : path.join(root, name) + ) + process.chdir(root) + + const presenter = await createPluginPresenter('win32', { appPath, arch: 'x64' }) + + await (presenter as any).loadOfficialPlugins() + + const resolvedPlugin = (presenter as any).officialPlugins.get(pluginId) + expect(resolvedPlugin.manifest.name).toBe('CUA Windows X64') + expect(resolvedPlugin.sourcePath).toBe(winX64Package) + }) + it('lists bundled official plugins as installed and enables them by materializing the package', async () => { const fixture = await createBundledFixture() const presenter = await createPluginPresenter('darwin', fixture.appPath) diff --git a/test/main/scripts/packagePlugin.test.ts b/test/main/scripts/packagePlugin.test.ts new file mode 100644 index 000000000..26593d0da --- /dev/null +++ b/test/main/scripts/packagePlugin.test.ts @@ -0,0 +1,128 @@ +import { spawnSync } from 'node:child_process' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { unzipSync } from 'fflate' +import { afterEach, describe, expect, it } from 'vitest' + +const ROOT = process.cwd() +const tempRoots: string[] = [] + +async function createCuaPluginFixture() { + const root = await mkdtemp(path.join(os.tmpdir(), 'deepchat-package-plugin-')) + tempRoots.push(root) + const pluginDir = path.join(root, 'cua') + const runtimeTargets = ['x64', 'arm64'] + const manifest = { + id: 'com.deepchat.plugins.cua', + name: 'Computer Use', + version: '0.0.0', + publisher: 'DeepChat', + engines: { + deepchat: '>=0.0.0', + platforms: ['darwin', 'win32', 'linux'], + targets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64'] + }, + activationEvents: ['onEnable'], + capabilities: ['runtime.manage', 'mcp.register'], + source: { + type: 'deepchat-official', + url: '${github.release.download}/deepchat-plugin-cua-${app.version}-${target.platform}-${arch}.dcplugin', + publisher: 'DeepChat' + }, + runtime: { + id: 'cua-driver', + type: 'external-helper', + displayName: 'CUA Driver', + detect: [ + 'plugin:runtime/darwin/${arch}/CuaDriver.app/Contents/MacOS/cua-driver', + 'plugin:runtime/win32/${arch}/cua-driver.exe', + 'plugin:runtime/linux/${arch}/cua-driver', + '/Applications/CuaDriver.app/Contents/MacOS/cua-driver' + ] + }, + mcpServers: [ + { + id: 'cua-driver', + displayName: 'CUA Driver', + transport: 'stdio', + command: '${runtime.cua-driver.command}', + args: [], + env: { + CUA_DRIVER_MCP_MODE: '1', + DEEPCHAT_COMPUTER_USE_APP_PATH: '${runtime.cua-driver.helperAppPath}', + DEEPCHAT_COMPUTER_USE_BINARY_PATH: '${runtime.cua-driver.command}' + }, + autoApprove: [] + } + ] + } + + await mkdir(pluginDir, { recursive: true }) + await writeFile(path.join(pluginDir, 'plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`) + for (const arch of runtimeTargets) { + const runtimeDir = path.join(pluginDir, 'runtime', 'win32', arch) + await mkdir(runtimeDir, { recursive: true }) + await writeFile(path.join(runtimeDir, 'cua-driver.exe'), 'driver') + await writeFile(path.join(runtimeDir, 'cua-driver-uia.exe'), 'uia') + } + + return { root, pluginDir } +} + +function runPackagePlugin(pluginDir: string, outDir: string, platform: string, arch: string) { + return spawnSync( + process.execPath, + [ + 'scripts/package-plugin.mjs', + '--out', + outDir, + '--target-platform', + platform, + '--target-arch', + arch, + pluginDir + ], + { + cwd: ROOT, + encoding: 'utf8' + } + ) +} + +describe('package-plugin', () => { + afterEach(async () => { + await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) + }) + + it('scopes packaged target metadata to the selected CUA artifact target', async () => { + const fixture = await createCuaPluginFixture() + const outDir = path.join(fixture.root, 'out') + + const result = runPackagePlugin(fixture.pluginDir, outDir, 'win32', 'arm64') + + if (result.status !== 0) { + throw new Error(result.stderr || result.stdout) + } + const artifactPath = path.join(outDir, 'deepchat-plugin-cua-0.0.0-win32-arm64.dcplugin') + const files = unzipSync(new Uint8Array(await readFile(artifactPath))) + const manifest = JSON.parse(Buffer.from(files['plugin.json']).toString('utf8')) + + expect(manifest.engines.targets).toEqual(['win32/arm64']) + expect(manifest.source.url).toContain('deepchat-plugin-cua-0.0.0-win32-arm64.dcplugin') + expect(Object.keys(files).filter((file) => file.startsWith('runtime/')).sort()).toEqual([ + 'runtime/win32/arm64/cua-driver-uia.exe', + 'runtime/win32/arm64/cua-driver.exe' + ]) + }) + + it('rejects unsupported CUA targets before scoped package metadata can make them visible', async () => { + const fixture = await createCuaPluginFixture() + const outDir = path.join(fixture.root, 'out') + + const result = runPackagePlugin(fixture.pluginDir, outDir, 'linux', 'arm64') + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain('Plugin com.deepchat.plugins.cua does not support linux/arm64') + }) +}) From 5145bedf8c459ced3c0b3427db6b11d2a231dc26 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 10:34:56 +0800 Subject: [PATCH 08/10] fix(cua): keep supported plugin active --- .../cua-cross-platform-computer-use/plan.md | 6 ++++ .../cua-cross-platform-computer-use/spec.md | 2 ++ .../cua-cross-platform-computer-use/tasks.md | 6 ++++ src/main/presenter/pluginPresenter/index.ts | 28 +++++++++++++++---- test/main/presenter/pluginPresenter.test.ts | 8 ++++++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/features/cua-cross-platform-computer-use/plan.md b/docs/features/cua-cross-platform-computer-use/plan.md index b8e4e6676..0604c1c49 100644 --- a/docs/features/cua-cross-platform-computer-use/plan.md +++ b/docs/features/cua-cross-platform-computer-use/plan.md @@ -122,6 +122,12 @@ Update plugin settings/runtime status code where needed: - Avoid macOS-only permission copy on non-macOS platforms. - Ensure missing Linux arm64 runtimes are reported as unsupported, not as broken installs. +### Plugin Discovery Cleanup + +Update official plugin discovery so unsupported sibling artifacts for the same plugin id do not +disable an already installed supported artifact. Only remove persisted plugin state when no trusted +candidate for the current platform/arch exists in the discovery pass. + ### Tests Update and add focused tests for: diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index eedab9a51..89a3716f0 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -215,6 +215,8 @@ The packaged app must keep CUA usable after Electron packaging: - Direct CUA runtime packaging for Linux arm64 fails with a clear unsupported-target message. - Official plugin visibility is gated by platform and arch, so the unsupported Linux arm target does not show CUA as available. +- Unsupported sibling artifacts for the same plugin id are ignored during discovery without + disabling or uninstalling an installed artifact that supports the current target. - The settings sidebar and settings routes expose the Plugins entry on supported CUA targets, not only on macOS, while keeping unsupported CUA targets hidden. - Runtime detection resolves the plugin-local binary on every supported target. diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md index 2777f93d8..22a0edaeb 100644 --- a/docs/features/cua-cross-platform-computer-use/tasks.md +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -91,6 +91,12 @@ - Use CI to validate Windows x64/arm64 packaging. - Confirm Linux arm64 jobs do not ship or show CUA. +- [x] T15 - Prevent unsupported sibling cleanup + - Keep unsupported target artifacts from clearing installed state when a supported artifact for + the same plugin id exists. + - Add regression coverage for side-by-side CUA target artifacts with an active plugin-owned tool + server. + ## Implementation Order 1. T01, T02, and T03 establish the runtime input and safety checks. diff --git a/src/main/presenter/pluginPresenter/index.ts b/src/main/presenter/pluginPresenter/index.ts index 04b873222..7e74b71d0 100644 --- a/src/main/presenter/pluginPresenter/index.ts +++ b/src/main/presenter/pluginPresenter/index.ts @@ -759,24 +759,42 @@ export class PluginPresenter { private async loadOfficialPlugins(): Promise { this.officialPlugins.clear() - - for (const plugin of [ + const plugins = [ ...this.resolveOfficialPluginPackages(), ...this.resolveOfficialPluginDirectories() - ]) { + ] + const usablePluginIds = new Set() + + for (const plugin of plugins) { + if (!this.isPluginPlatformSupported(plugin.manifest)) { + continue + } + try { + this.assertTrustedOfficialPlugin(plugin.manifest) + usablePluginIds.add(plugin.manifest.id) + } catch { + // The main discovery pass logs untrusted plugin details and performs cleanup. + } + } + + for (const plugin of plugins) { if (this.officialPlugins.has(plugin.manifest.id)) { continue } if (!this.isPluginPlatformSupported(plugin.manifest)) { console.info(`[PluginHost] Skipping plugin ${plugin.manifest.id}: platform not supported`) - await this.removePersistedInstallation(plugin.manifest.id) + if (!usablePluginIds.has(plugin.manifest.id)) { + await this.removePersistedInstallation(plugin.manifest.id) + } continue } try { this.assertTrustedOfficialPlugin(plugin.manifest) } catch (error) { console.warn(`[PluginHost] Skipping untrusted plugin ${plugin.manifest.id}:`, error) - await this.removePersistedInstallation(plugin.manifest.id) + if (!usablePluginIds.has(plugin.manifest.id)) { + await this.removePersistedInstallation(plugin.manifest.id) + } continue } console.info(`[PluginHost] Discovered plugin: ${plugin.manifest.id} at ${plugin.root}`) diff --git a/test/main/presenter/pluginPresenter.test.ts b/test/main/presenter/pluginPresenter.test.ts index d21590aa5..995f2c05c 100644 --- a/test/main/presenter/pluginPresenter.test.ts +++ b/test/main/presenter/pluginPresenter.test.ts @@ -431,12 +431,20 @@ describe('PluginPresenter', () => { process.chdir(root) const presenter = await createPluginPresenter('win32', { appPath, arch: 'x64' }) + await presenter.__mocks.configPresenter.addMcpServer('cua-driver', { + ownerPluginId: pluginId, + source: 'plugin', + sourceId: pluginId + }) + presenter.__mocks.mcpPresenter.isServerRunning.mockResolvedValue(true) await (presenter as any).loadOfficialPlugins() const resolvedPlugin = (presenter as any).officialPlugins.get(pluginId) expect(resolvedPlugin.manifest.name).toBe('CUA Windows X64') expect(resolvedPlugin.sourcePath).toBe(winX64Package) + expect(presenter.__mocks.mcpPresenter.stopServer).not.toHaveBeenCalled() + expect(presenter.__mocks.configPresenter.removeMcpServer).not.toHaveBeenCalled() }) it('lists bundled official plugins as installed and enables them by materializing the package', async () => { From f1b7b9cbd1bba97f3b75a4da9e5370ff99db60b8 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 10:51:09 +0800 Subject: [PATCH 09/10] fix(cua): quiet optional mcp probes --- .../cua-cross-platform-computer-use/spec.md | 2 + .../cua-cross-platform-computer-use/tasks.md | 5 ++ src/main/presenter/mcpPresenter/mcpClient.ts | 24 +++--- test/main/presenter/mcpClient.test.ts | 74 ++++++++++++++++++- 4 files changed, 93 insertions(+), 12 deletions(-) diff --git a/docs/features/cua-cross-platform-computer-use/spec.md b/docs/features/cua-cross-platform-computer-use/spec.md index 89a3716f0..f43ed7d88 100644 --- a/docs/features/cua-cross-platform-computer-use/spec.md +++ b/docs/features/cua-cross-platform-computer-use/spec.md @@ -221,6 +221,8 @@ The packaged app must keep CUA usable after Electron packaging: only on macOS, while keeping unsupported CUA targets hidden. - Runtime detection resolves the plugin-local binary on every supported target. - The plugin starts through DeepChat's internal tool path without user-managed MCP setup. +- Optional MCP capabilities not implemented by the CUA driver, such as prompts and resources, are + treated as absent capabilities and must not produce error-level log spam. - Skill docs describe DeepChat usage and platform caveats, not upstream manual installer workflows. - Tool policies cover all upstream v0.5.5 tools known to this integration. - Packaging docs and tests no longer describe CUA as macOS-only. diff --git a/docs/features/cua-cross-platform-computer-use/tasks.md b/docs/features/cua-cross-platform-computer-use/tasks.md index 22a0edaeb..ce843c9dc 100644 --- a/docs/features/cua-cross-platform-computer-use/tasks.md +++ b/docs/features/cua-cross-platform-computer-use/tasks.md @@ -97,6 +97,11 @@ - Add regression coverage for side-by-side CUA target artifacts with an active plugin-owned tool server. +- [x] T16 - Quiet unsupported optional MCP capabilities + - Treat `-32601 Unknown method` from prompts and resources list requests as unsupported optional + capabilities. + - Cache empty prompts/resources lists so CUA does not repeatedly emit error stack traces. + ## Implementation Order 1. T01, T02, and T03 establish the runtime input and safety checks. diff --git a/src/main/presenter/mcpPresenter/mcpClient.ts b/src/main/presenter/mcpPresenter/mcpClient.ts index 8517114af..ff0c90171 100644 --- a/src/main/presenter/mcpPresenter/mcpClient.ts +++ b/src/main/presenter/mcpPresenter/mcpClient.ts @@ -108,6 +108,14 @@ function isSessionError(error: unknown): error is SessionError { return false } +function isUnsupportedCapabilityError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error) + if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) { + return true + } + return /method not found|unknown method|not supported|unsupported|mcp error -32601/i.test(message) +} + // MCP client class export class McpClient { private client: Client | null = null @@ -1032,10 +1040,8 @@ export class McpClient { // 检查并处理session错误 await this.checkAndHandleSessionError(error) - // 尝试从错误对象中提取更多信息 - const errorMessage = error instanceof Error ? error.message : String(error) // 如果错误表明不支持,则缓存空数组 - if (errorMessage.includes('Method not found') || errorMessage.includes('not supported')) { + if (isUnsupportedCapabilityError(error)) { console.warn(`Server ${this.serverName} does not support listTools`) this.cachedTools = [] return this.cachedTools @@ -1095,11 +1101,9 @@ export class McpClient { // 检查并处理session错误 await this.checkAndHandleSessionError(error) - // 尝试从错误对象中提取更多信息 - const errorMessage = error instanceof Error ? error.message : String(error) // 如果错误表明不支持,则缓存空数组 - if (errorMessage.includes('Method not found') || errorMessage.includes('not supported')) { - console.warn(`Server ${this.serverName} does not support listPrompts`) + if (isUnsupportedCapabilityError(error)) { + console.info(`Server ${this.serverName} does not support listPrompts`) this.cachedPrompts = [] return this.cachedPrompts } else { @@ -1196,11 +1200,9 @@ export class McpClient { // 检查并处理session错误 await this.checkAndHandleSessionError(error) - // 尝试从错误对象中提取更多信息 - const errorMessage = error instanceof Error ? error.message : String(error) // 如果错误表明不支持,则缓存空数组 - if (errorMessage.includes('Method not found') || errorMessage.includes('not supported')) { - console.warn(`Server ${this.serverName} does not support listResources`) + if (isUnsupportedCapabilityError(error)) { + console.info(`Server ${this.serverName} does not support listResources`) this.cachedResources = [] return this.cachedResources } else { diff --git a/test/main/presenter/mcpClient.test.ts b/test/main/presenter/mcpClient.test.ts index 6f5c01c57..5a462e3be 100644 --- a/test/main/presenter/mcpClient.test.ts +++ b/test/main/presenter/mcpClient.test.ts @@ -405,7 +405,79 @@ describe('McpClient Runtime Command Processing Tests', () => { expect(transportOptions.env.TOKEN).toBe('123') expect(transportOptions.env.EMPTY).toBe('') expect(transportOptions.env).not.toHaveProperty('SKIP') - expect(transportOptions.env.PATH).toContain('/custom/bin') + const pathEnv = + transportOptions.env.PATH ?? transportOptions.env.Path ?? transportOptions.env.path + expect(pathEnv).toContain('/custom/bin') + }) + }) + + describe('Unsupported MCP capabilities', () => { + it('treats unknown prompts/list as an empty prompt list', async () => { + const sdkClient = { + connect: vi.fn().mockResolvedValue(undefined), + callTool: vi.fn(), + listTools: vi.fn(), + listPrompts: vi + .fn() + .mockRejectedValue( + new McpError(ErrorCode.MethodNotFound, 'Unknown method: prompts/list') + ), + getPrompt: vi.fn(), + listResources: vi.fn(), + readResource: vi.fn(), + setNotificationHandler: vi.fn(), + setRequestHandler: vi.fn() + } + vi.mocked(Client).mockImplementationOnce(() => sdkClient as any) + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + const client = new McpClient('cua-driver', { + type: 'stdio', + command: 'cua-driver', + args: ['mcp'] + }) + + await expect(client.listPrompts()).resolves.toEqual([]) + await expect(client.listPrompts()).resolves.toEqual([]) + + expect(sdkClient.listPrompts).toHaveBeenCalledTimes(1) + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Failed to list MCP prompts:'), + expect.anything() + ) + consoleErrorSpy.mockRestore() + }) + + it('treats unknown resources/list as an empty resource list', async () => { + const sdkClient = { + connect: vi.fn().mockResolvedValue(undefined), + callTool: vi.fn(), + listTools: vi.fn(), + listPrompts: vi.fn(), + getPrompt: vi.fn(), + listResources: vi + .fn() + .mockRejectedValue(new Error('MCP error -32601: Unknown method: resources/list')), + readResource: vi.fn(), + setNotificationHandler: vi.fn(), + setRequestHandler: vi.fn() + } + vi.mocked(Client).mockImplementationOnce(() => sdkClient as any) + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + const client = new McpClient('cua-driver', { + type: 'stdio', + command: 'cua-driver', + args: ['mcp'] + }) + + await expect(client.listResources()).resolves.toEqual([]) + await expect(client.listResources()).resolves.toEqual([]) + + expect(sdkClient.listResources).toHaveBeenCalledTimes(1) + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Failed to list MCP resources:'), + expect.anything() + ) + consoleErrorSpy.mockRestore() }) }) From 01e9dff6d23c43e155586ce284d2c8a71fd307de Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 17 Jun 2026 12:21:04 +0800 Subject: [PATCH 10/10] fix(cua): guard windows launch diagnostics --- .../plan.md | 36 ++++ .../spec.md | 42 ++++ .../tasks.md | 8 + plugins/cua/settings/assets/index.css | 16 ++ plugins/cua/settings/assets/index.js | 158 ++++++++++++-- plugins/cua/settings/index.html | 14 +- plugins/cua/skills/cua-driver/SKILL.md | 5 +- plugins/cua/types/settings-preload.d.ts | 3 + .../presenter/mcpPresenter/toolManager.ts | 192 +++++++++++++++++- src/main/presenter/pluginPresenter/index.ts | 98 ++++++++- src/preload/plugin-settings-preload.ts | 2 + src/shared/types/plugin.ts | 2 + .../mcpPresenter/toolManager.test.ts | 54 +++++ test/main/presenter/pluginPresenter.test.ts | 32 +++ 14 files changed, 620 insertions(+), 42 deletions(-) create mode 100644 docs/issues/cua-windows-launch-timeout-permissions/plan.md create mode 100644 docs/issues/cua-windows-launch-timeout-permissions/spec.md create mode 100644 docs/issues/cua-windows-launch-timeout-permissions/tasks.md diff --git a/docs/issues/cua-windows-launch-timeout-permissions/plan.md b/docs/issues/cua-windows-launch-timeout-permissions/plan.md new file mode 100644 index 000000000..0b1e02494 --- /dev/null +++ b/docs/issues/cua-windows-launch-timeout-permissions/plan.md @@ -0,0 +1,36 @@ +# Plan + +## Diagnosis + +Local packaged CUA `cua-driver.exe mcp` can list tools and run `launch_app` when the target is +resolvable. On Windows, `launch_app` waits for the MCP SDK default timeout when called with +unresolvable names or macOS-style bundle ids. The settings-page permission failure is a DeepChat +parsing and presentation issue: Windows `check_permissions` returns JSON with `post_message`, +`uia`, elevation, and integrity fields rather than macOS Accessibility and Screen Recording text. + +## Implementation + +- Add a CUA-specific launch guard in `ToolManager` for plugin-owned + `com.deepchat.plugins.cua` servers on Windows. +- Before dispatching `launch_app`, normalize Windows path-like `bundle_id` values to `path`. +- For `name` or plain `bundle_id` requests, call `list_apps` and match against known Windows app + identifiers before dispatch. If there is no match, return a tool error immediately. +- Extend CUA runtime permission results with `platform` and a diagnostics object. +- Parse JSON from non-macOS `check_permissions` output. +- Update the CUA settings page to render platform-specific rows: + - macOS: Accessibility, Screen Recording + - Windows: UI Automation, PostMessage, Integrity Level, Elevated + - Linux: permission check unavailable or runtime diagnostics + +## Test Strategy + +- Unit test the CUA launch guard in `ToolManager`. +- Unit test Windows JSON permission parsing in `PluginPresenter`. +- Run focused tests for touched presenters. +- Run project-required quality gates after implementation: format, i18n, lint. + +## Compatibility + +Existing macOS CUA permission probe and existing CUA tool names remain unchanged. The Windows guard +only intercepts unresolved or platform-mismatched `launch_app` inputs that previously hung. + diff --git a/docs/issues/cua-windows-launch-timeout-permissions/spec.md b/docs/issues/cua-windows-launch-timeout-permissions/spec.md new file mode 100644 index 000000000..544eee1c1 --- /dev/null +++ b/docs/issues/cua-windows-launch-timeout-permissions/spec.md @@ -0,0 +1,42 @@ +# CUA Windows Launch Timeout And Permission Diagnostics + +## Problem + +Packaged CUA on Windows can connect and expose tools, but `launch_app` may hang until the MCP SDK +request timeout when the model supplies an app identifier that the Windows driver cannot resolve. +The CUA plugin settings page also presents macOS permission labels on Windows and turns Windows +permission diagnostics into a generic failure message. + +## User Stories + +- As a Windows user, I want invalid or platform-mismatched `launch_app` arguments to fail quickly + with actionable guidance instead of waiting for `MCP error -32001: Request timed out`. +- As a Windows or Linux user, I want the CUA settings page to show platform-relevant diagnostics + instead of macOS Accessibility and Screen Recording checks. +- As a macOS user, I want the existing helper permission flow to keep working unchanged. + +## Acceptance Criteria + +- Windows CUA `launch_app` calls are preflighted before dispatching to the MCP driver when the + request uses a free-form `name` or non-AUMID `bundle_id`. +- Windows desktop app paths carried in `bundle_id` are normalized to the Windows `path` argument + before calling the driver. +- Windows unresolved app names or macOS-style bundle ids return an immediate tool error that tells + the agent to call `list_apps` and use `name`, `path`, `launch_path`, or `aumid`. +- CUA permission checks return platform-specific fields for Windows and Linux without marking a + successful Windows JSON check as a failure. +- The settings page renders platform-specific permission/diagnostic labels and messages. +- Tests cover Windows launch preflight and permission parsing behavior. + +## Non-Goals + +- Modify upstream `trycua/cua` binaries. +- Replace the plugin-owned MCP transport with a non-MCP tool host. +- Add new CUA-supported platforms beyond the current support matrix. + +## Constraints + +- Keep changes scoped to the CUA integration and generic MCP wrapper behavior needed by CUA. +- Do not expose plugin-owned CUA MCP servers in the normal MCP settings UI. +- Preserve macOS permission probe behavior. + diff --git a/docs/issues/cua-windows-launch-timeout-permissions/tasks.md b/docs/issues/cua-windows-launch-timeout-permissions/tasks.md new file mode 100644 index 000000000..1f1852901 --- /dev/null +++ b/docs/issues/cua-windows-launch-timeout-permissions/tasks.md @@ -0,0 +1,8 @@ +# Tasks + +- [x] T1 Document packaged CUA Windows launch timeout and platform diagnostics requirements. +- [x] T2 Add Windows CUA `launch_app` preflight and argument normalization. +- [x] T3 Parse and return platform-specific runtime permission diagnostics. +- [x] T4 Update CUA settings page rendering for macOS, Windows, and Linux. +- [x] T5 Add focused tests for launch guard and Windows permission parsing. +- [x] T6 Run verification commands and summarize residual risks. diff --git a/plugins/cua/settings/assets/index.css b/plugins/cua/settings/assets/index.css index d7cb089c5..04ab08004 100644 --- a/plugins/cua/settings/assets/index.css +++ b/plugins/cua/settings/assets/index.css @@ -97,6 +97,17 @@ h1 { border-top: 0; } +.section-title { + min-height: 36px; + padding: 10px 12px 8px; + border-bottom: 1px solid #e5e5df; + color: #52525b; + font-size: 12px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + .row span { color: #52525b; font-size: 13px; @@ -199,6 +210,11 @@ button.danger:hover { border-color: #2f2f33; } + .section-title { + border-color: #2f2f33; + color: #a1a1aa; + } + .row span, .message, .eyebrow, diff --git a/plugins/cua/settings/assets/index.js b/plugins/cua/settings/assets/index.js index 880abffe2..403ce24a7 100644 --- a/plugins/cua/settings/assets/index.js +++ b/plugins/cua/settings/assets/index.js @@ -1,14 +1,18 @@ const stateNode = document.getElementById('plugin-state') const runtimeStateNode = document.getElementById('runtime-state') const runtimeVersionNode = document.getElementById('runtime-version') +const runtimePlatformNode = document.getElementById('runtime-platform') const runtimeCommandNode = document.getElementById('runtime-command') const runtimeHelperAppNode = document.getElementById('runtime-helper-app') const mcpStateNode = document.getElementById('mcp-state') -const accessibilityNode = document.getElementById('permission-accessibility') -const screenRecordingNode = document.getElementById('permission-screen-recording') +const diagnosticsTitleNode = document.getElementById('diagnostics-title') +const diagnosticsRowsNode = document.getElementById('diagnostics-rows') const messageNode = document.getElementById('message') const projectLinkNode = document.getElementById('project-link') +let currentPlatform = 'unknown' +let currentArch = 'unknown' + function setText(node, value) { if (node) { node.textContent = value || 'Unknown' @@ -29,45 +33,156 @@ function setState(enabled) { stateNode.className = enabled ? 'state state-ok' : 'state state-muted' } -function setPermissionStatus(node, value) { - if (!node) { - return +function getPluginApi() { + const api = window.deepchatPlugin + if (!api) { + throw new Error( + 'DeepChat plugin settings bridge is unavailable. Restart DeepChat and reopen this page.' + ) } + return api +} +function normalizeStatus(value) { const normalized = String(value || '').toLowerCase() if (normalized === 'granted') { - node.textContent = 'Granted' - node.className = 'permission-pill permission-ok' + return { text: 'Granted', className: 'permission-pill permission-ok' } + } + if (normalized === 'missing' || normalized === 'denied' || normalized === 'deny') { + return { text: 'Denied', className: 'permission-pill permission-denied' } + } + if (normalized === 'available' || normalized === 'ready' || normalized === 'ok') { + return { text: 'Ready', className: 'permission-pill permission-ok' } + } + if (normalized === 'unavailable' || normalized === 'failed') { + return { text: 'Unavailable', className: 'permission-pill permission-denied' } + } + return { text: value || 'Unknown', className: 'permission-pill permission-muted' } +} + +function createRow(label, value, statusValue) { + const row = document.createElement('div') + row.className = 'row' + + const labelNode = document.createElement('span') + labelNode.textContent = label + row.appendChild(labelNode) + + const valueNode = document.createElement('strong') + const status = normalizeStatus(statusValue || value) + valueNode.textContent = status.text + valueNode.className = status.className + row.appendChild(valueNode) + + return row +} + +function renderDiagnostics(title, rows) { + if (diagnosticsTitleNode) { + diagnosticsTitleNode.textContent = title + } + if (!diagnosticsRowsNode) { return } + diagnosticsRowsNode.textContent = '' + for (const row of rows) { + diagnosticsRowsNode.appendChild(createRow(row.label, row.value, row.status)) + } +} - if (normalized === 'missing' || normalized === 'denied' || normalized === 'deny') { - node.textContent = 'Denied' - node.className = 'permission-pill permission-denied' +function renderInitialDiagnostics(platform) { + if (platform === 'darwin') { + renderDiagnostics('macOS Permissions', [ + { label: 'Accessibility', value: 'Run Check' }, + { label: 'Screen Recording', value: 'Run Check' } + ]) return } + if (platform === 'win32') { + renderDiagnostics('Windows Diagnostics', [ + { label: 'UI Automation', value: 'Run Check' }, + { label: 'PostMessage', value: 'Run Check' }, + { label: 'Integrity Level', value: 'Run Check' }, + { label: 'Elevated', value: 'Run Check' } + ]) + return + } + if (platform === 'linux') { + renderDiagnostics('Linux Diagnostics', [{ label: 'Runtime Check', value: 'Run Check' }]) + return + } + renderDiagnostics('Diagnostics', [{ label: 'Runtime Check', value: 'Run Check' }]) +} - node.textContent = 'Unavailable' - node.className = 'permission-pill permission-muted' +function asRecord(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {} } -function getPluginApi() { - const api = window.deepchatPlugin - if (!api) { - throw new Error( - 'DeepChat plugin settings bridge is unavailable. Restart DeepChat and reopen this page.' - ) +function formatBoolean(value) { + if (typeof value !== 'boolean') { + return 'Unknown' } - return api + return value ? 'Yes' : 'No' +} + +function renderPermissionResult(data) { + const record = asRecord(data) + const platform = String(record.platform || currentPlatform) + const diagnostics = asRecord(record.diagnostics) + + if (platform === 'darwin') { + renderDiagnostics('macOS Permissions', [ + { label: 'Accessibility', value: record.accessibility }, + { label: 'Screen Recording', value: record.screenRecording } + ]) + return + } + + if (platform === 'win32') { + renderDiagnostics('Windows Diagnostics', [ + { label: 'UI Automation', value: record.uia }, + { label: 'PostMessage', value: record.postMessage }, + { + label: 'Integrity Level', + value: diagnostics.integrity_level || diagnostics.integrityLevel || 'Unknown' + }, + { label: 'Elevated', value: formatBoolean(diagnostics.elevated) } + ]) + return + } + + if (platform === 'linux') { + renderDiagnostics('Linux Diagnostics', [ + { + label: 'Runtime Check', + value: record.error ? 'Unavailable' : 'Ready', + status: record.error ? 'unavailable' : 'ready' + } + ]) + return + } + + renderDiagnostics('Diagnostics', [ + { + label: 'Runtime Check', + value: record.error ? 'Unavailable' : 'Ready', + status: record.error ? 'unavailable' : 'ready' + } + ]) } async function refreshStatus() { const status = await getPluginApi().getStatus() + currentPlatform = status.platform || 'unknown' + currentArch = status.arch || 'unknown' + setState(status.enabled) setText(runtimeStateNode, status.runtime?.state) setText(runtimeVersionNode, status.runtime?.version) + setText(runtimePlatformNode, `${currentPlatform}/${currentArch}`) setText(runtimeCommandNode, status.runtime?.command) setText(runtimeHelperAppNode, status.runtime?.helperAppPath || 'Not required on this platform') + renderInitialDiagnostics(currentPlatform) const cuaMcp = status.mcpServers?.find((server) => server.serverId === 'cua-driver') if (!cuaMcp) { @@ -89,7 +204,7 @@ async function refreshStatus() { } async function checkPermissions() { - setMessage('Checking permissions...') + setMessage('Checking diagnostics...') const result = await getPluginApi().invokeAction('runtime.checkPermissions') if (!result.ok || !result.data) { console.error('[CUA Settings] Permission check failed:', result) @@ -97,8 +212,7 @@ async function checkPermissions() { return } - setPermissionStatus(accessibilityNode, result.data.accessibility) - setPermissionStatus(screenRecordingNode, result.data.screenRecording) + renderPermissionResult(result.data) if (result.data.error) { console.warn('[CUA Settings] Permission check returned diagnostics:', result.data) setMessage(result.data.error) diff --git a/plugins/cua/settings/index.html b/plugins/cua/settings/index.html index 068293d4f..22e897b7e 100644 --- a/plugins/cua/settings/index.html +++ b/plugins/cua/settings/index.html @@ -29,6 +29,10 @@

CUA Computer Use Runtime

Version Unknown +
+ Platform + Unknown +
Command Unknown @@ -44,14 +48,8 @@

CUA Computer Use Runtime

-
- Accessibility - Unknown -
-
- Screen Recording - Unknown -
+
Permissions
+