dsh-codex-style-computer-use 1.2.0 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,23 @@
1
- # Computer Use · Codex-style desktop control
1
+ # dsh-codex-style-computer-use · Codex-style desktop control
2
+
3
+ **Give DeepSeek eyes and hands for Windows.**
4
+
5
+ Ask your agent to work with desktop apps: see the screen, find controls, click and type. Take over with your own mouse or keyboard whenever needed; the agent waits before continuing. Approval prompts remain enabled by default.
2
6
 
3
7
  Windows desktop computer use for **DeepSeek Harness**: 18 desktop tools plus an Agent activation entry that observe and drive
4
8
  native Windows applications through prebuilt C# workers included in the package. No PowerShell 7,
5
- SDK, terminal commands or install-time builds are needed.
9
+ SDK or install-time builds are needed.
6
10
 
7
11
  English | [中文](README.zh.md)
8
12
 
9
13
  Windows only (`os: win32`). Requires Node.js ≥ 22 and the .NET Framework 4.x that Windows ships with.
10
14
 
15
+ **1.2.3 automatic fix:** normal installation/update now installs the approval-attribution correction for computer-use calls on affected DSH hosts. No manual host patch is required for those calls. Host approvals and audit remain authoritative; follow the host's restart indication after updating an already loaded plugin. See [1.2.3](docs/RELEASE-1.2.3.md).
16
+
17
+ **1.2.1 attention fix:** fixes a missing crypto import that prevented 1.2.0's native `probe` and `raise` requests from reaching the helper. Updating from 1.2.0 is required for this runtime fix; reinstalling 1.2.0 does not fix it. New releases are staged on npm's `next` tag before promotion to `latest`; the market's normal update follows `latest`. See [1.2.1 validation and release status](docs/RELEASE-1.2.1.md).
18
+
19
+ **1.2.2 GitHub pre-release:** includes a separately deployed DSH host approval-attribution patch and 14 regression cases. Plugin input and approval behavior is retained from 1.2.1. See [release contents and deployment scope](docs/RELEASE-1.2.2.md); GitHub publication does not update npm or the market automatically.
20
+
11
21
  ## What it does
12
22
 
13
23
  - **Observe** — window list, screenshots (region / JPEG / PNG / downscaled), a Set-of-Marks map of
@@ -27,12 +37,35 @@ under `$DSH_HOME/data/computer-use/`.
27
37
 
28
38
  ## Install and update in dsh-market
29
39
 
30
- Search **computer-use** and select **dsh-codex-style-computer-use**, by **ltmroberthk915** (npm maintainer: **ltmroberthk**). Click **Install**. Later, click **Update** or **Update all** in the market. The package includes both native helpers; no PowerShell 7 or build permission is needed.
40
+ Once the catalog submission is merged and synced, search **computer-use** and select **dsh-codex-style-computer-use**, by **ltmroberthk915** (npm maintainer: **ltmroberthk**). Click **Install**. Later, use **Update** or **Update all** for versions admitted by the host's release policy. The package includes both native helpers; no PowerShell 7 or build permission is needed.
41
+
42
+ Before catalog sync, use **Settings → Plugins → Add plugin**. The corrected version is **`dsh-codex-style-computer-use@1.2.1`**; the unversioned name still resolves to 1.2.0 until promotion. Do not paste the GitHub repository URL: that selects the source-download path instead of the small prebuilt npm package. The default non-strict pnpm 11.7.0 configuration can install 1.2.1 immediately; an explicitly strict host policy can require waiting as described below. A newly added bundle can load live on the official Desktop host; check that its tools and skill appear. Restart from the tray when replacing an already loaded version, when the client remains stale, or when DSH reports **restart-required**.
43
+
44
+ ### Releases less than 24 hours old
45
+
46
+ The default non-strict pnpm 11.7.0 configuration automatically records a single-version exception and installs. A successful install on one machine does not establish that another machine permits fresh versions.
47
+
48
+ With `minimumReleaseAgeStrict: true` (or a host policy that enables strict release-age checks), and no applicable exception, a version below the configured age is rejected before plugin code runs. The bare name, an exact `@1.2.0` pin, and the npm tarball URL were all verified to fail with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` under that policy. Explicitly setting `minimumReleaseAge: 1440` also enables strict behavior in pnpm 11.7.0 unless `minimumReleaseAgeStrict` is separately set to false. Restarting DSH, installing PowerShell, or retrying the same command does not fix a strict-policy refusal. The official desktop bridge does not accept extra pnpm flags.
49
+
50
+ If a strict policy blocks installation, wait until the version meets the configured age to install without changing policy. Version 1.2.0 was published at `2026-10-03T03:50:11.586Z`; it meets a 24-hour cutoff after **2026-10-04 03:50:12 UTC** (11:50:12 in China). A longer custom cutoff or a lagging registry mirror can delay availability further. Default non-strict users do not need to wait or edit configuration for this package.
51
+
52
+ If you explicitly choose to install earlier, merge this single-version exception into the target profile's existing `pnpm-workspace.yaml`, then retry the market install. Preserve other entries; do not disable the global age policy:
53
+
54
+ ```yaml
55
+ minimumReleaseAgeExclude:
56
+ - dsh-codex-style-computer-use@1.2.0
57
+ ```
31
58
 
32
- If the catalog has not synced yet, use **Settings → Plugins → Add plugin**, enter `dsh-codex-style-computer-use`, and install. This also creates an ordinary registry installation that can be updated from the market. If DSH reports **restart required**, fully exit DSH from its tray menu and reopen it.
59
+ The default desktop path is `%USERPROFILE%\.dsh\profiles\desktop\pnpm-workspace.yaml`; use the actual directory if `DSH_HOME` is customized. Remove this one exception after the version matures if desired.
60
+
61
+ The earlier documentation and publisher-check corrections did not change the 1.2.0 runtime and needed no reinstall. The native attention fix in **1.2.1** does require an update. Re-adding the same source can produce a host `ambiguous-install` error on some installation paths; reinstalling 1.2.0 will not apply the 1.2.1 fix.
33
62
 
34
63
  **Migrating an older Git/tarball installation from this repository:** remove the old **dsh-computer-use** entry in the market, then install **dsh-codex-style-computer-use**. The unscoped npm name `dsh-computer-use` belongs to a different repository; do not install it as an upgrade of this plugin. A Git installation cannot switch its dependency identity just by fetching a new commit. After this one-time UI migration, use normal market updates. Existing `computer_*` tool names and the `computer-use` settings namespace are retained.
35
64
 
65
+ **Preserve your saved configuration too:** back up the profile's `cordis.patch.yml`, then change only `name: dsh-computer-use` on an override with `id: computer-use` to `name: dsh-codex-style-computer-use`. Keep its entire `config` and `disabled` fields. Retaining the settings namespace alone is insufficient: a name mismatch makes DSH skip even `read-only`, `dryRun`, and disabled-state overrides. Do not replace other plugins or a shared home patch globally. The repository's `scripts/migrate-profile.mjs` previews by default; `--apply` creates a backup and replaces the file atomically, refusing concurrent edits.
66
+
67
+ Alternatively, download the [configuration migration utility](https://github.com/ltmroberthk915/dsh-computer-use/releases/download/v1.2.0/computer-use-profile-migration.zip), extract it and double-click `migrate-profile.cmd`. It locates the installed official Desktop app and uses Windows PowerShell 5.1 plus DSH's bundled Node to repair the default desktop profile. It stops if the new bundle is absent or the old bundle is still selected. It does not change release-age policy. For another profile, run `migrate-profile.ps1 -ProfileDirectory <absolute-directory>` to preview, then add `-Apply` when ready.
68
+
36
69
  The GitHub Release also includes `dsh-computer-use.tgz` for offline/manual installation; registry installation is the default for market updates.
37
70
 
38
71
  CLI users (web profile):
@@ -41,6 +74,15 @@ CLI users (web profile):
41
74
  dsh plugin --profile web add dsh-codex-style-computer-use
42
75
  ```
43
76
 
77
+ Desktop diagnostics must use the **CLI bundled with the official Desktop app**:
78
+
79
+ ```powershell
80
+ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
81
+ & "$CuDshInstall\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop why dsh-codex-style-computer-use
82
+ ```
83
+
84
+ For a custom installation, set `$CuDshInstall` to the directory in the Desktop shortcut's target. An older global npm `dsh` can reject the desktop profile. Do not add `resources/runtime/bin` to the global PATH or install a separate pnpm for this task. Prefer the plugin UI for installation.
85
+
44
86
  ## The bundled skill
45
87
 
46
88
  The driving tools stay locked until the session produces a receipt phrase that exists only inside
@@ -79,6 +121,8 @@ clicked at the wrong place.
79
121
 
80
122
  ## Approval modes
81
123
 
124
+ For a host that reports a disabled-prompt automatic refusal as “the user rejected tool”, see the [DSH approval attribution patch and regression test](docs/APPROVAL-ATTRIBUTION.md).
125
+
82
126
  `automationMode` is set in the plugin's settings and defaults to `standard`:
83
127
 
84
128
  | Mode | Observation | Actuation |
@@ -127,6 +171,16 @@ node mcp/src/index.js
127
171
 
128
172
  ## Development
129
173
 
174
+ Publisher release checks use npm's official registry by default. If that origin is unreachable on your network, `CU_REGISTRY` selects an explicit mirror for the read-only `check` command; the JSON result includes `registry` and `authoritative`. Example in PowerShell (this changes only the current shell):
175
+
176
+ ```powershell
177
+ $env:CU_REGISTRY = 'https://registry.npmmirror.com'
178
+ node scripts/release-channel.mjs check 1.2.0
179
+ Remove-Item Env:CU_REGISTRY
180
+ ```
181
+
182
+ Exit code `0` means the version has passed the 24-hour publisher check, `2` means it is still cooling, and `1` means the check failed. A network failure does not establish that a package is unpublished, and mirrors can lag. `promote` always rechecks and writes `https://registry.npmjs.org`, ignoring `CU_REGISTRY`; it needs access to that origin, using npm's proxy configuration when necessary. This publisher tool is not part of end-user installation. See [the release procedure](docs/RELEASE-1.2.0.md).
183
+
130
184
  The repository ships the guard suite that protects the invariants above — the cycle lifetime, the
131
185
  ask/brake protocol, the operation classification, the skill gate, and the bundled-skill wiring:
132
186
 
package/README.zh.md CHANGED
@@ -1,4 +1,8 @@
1
- # Computer Use · Codex 风格桌面操控
1
+ # dsh-codex-style-computer-use · Codex 风格桌面操控
2
+
3
+ **让 DeepSeek 看得见,也点得动。**
4
+
5
+ 把 Windows 应用里的重复操作交给 AI:看屏幕、找控件、点鼠标、敲键盘。你一碰鼠标键盘,它就让出控制,等你操作完再继续;默认保留操作审批。
2
6
 
3
7
  给 **DeepSeek Harness** 用的 Windows 桌面操控插件:18 个桌面工具加一个 Agent 激活入口,通过一个 C# worker
4
8
  观察并驱动原生 Windows 应用。发布包已包含编译好的原生组件,无需安装 PowerShell 7、SDK 或执行编译命令。
@@ -7,6 +11,14 @@
7
11
 
8
12
  仅支持 Windows(`os: win32`)。需要 Node.js ≥ 22 与 Windows 自带的 .NET Framework 4.x。
9
13
 
14
+ **1.2.3 自动修复:**正常安装或更新插件,即会为受影响宿主中的 computer-use 调用安装审批归因修正,无需手工修改宿主。审批决定与审计仍由宿主处理;更新已加载插件后按宿主提示重启。详见 [1.2.3](docs/RELEASE-1.2.3.md)。
15
+
16
+ **1.2.1 提醒修复:**补齐 crypto 导入,修复 1.2.0 原生 `probe` / `raise` 请求还未发送就抛错的问题。这次是运行时代码修复,需要从 1.2.0 更新;重复安装 1.2.0 无效。新版本先发布到 npm 的 `next`,再提升为 `latest`,市场普通更新跟随 `latest`。详见 [1.2.1 验证和发布状态](docs/RELEASE-1.2.1.md)。
17
+
18
+ 宿主在禁用审批弹窗时仍提示“用户拒绝工具”的问题,详见 [DSH 审批归因补丁与回归测试](docs/APPROVAL-ATTRIBUTION.md)。
19
+
20
+ **1.2.2 GitHub 预发布:**收录独立部署的宿主审批归因补丁和 14 项回归测试。插件输入与审批行为延续 1.2.1,详见 [发布内容及部署范围](docs/RELEASE-1.2.2.md)。GitHub 发布不会自动更新 npm 或市场版本。
21
+
10
22
  ## 它能做什么
11
23
 
12
24
  - **观察** —— 窗口列表、截图(区域 / JPEG / PNG / 降采样)、可点元素的 Set-of-Marks 编号图,
@@ -23,12 +35,35 @@
23
35
 
24
36
  ## 在 dsh-market 安装和更新
25
37
 
26
- 搜索 **computer-use**,选择 **dsh-codex-style-computer-use**,作者 **ltmroberthk915**(npm 维护者 **ltmroberthk**),点击**安装**。以后直接点市场里的**更新**或**全部更新**。两份原生组件都已随包提供,无需终端、PowerShell 7、SDK 或手改构建白名单。
38
+ 市场收录同步后,搜索 **computer-use**,选择 **dsh-codex-style-computer-use**,作者 **ltmroberthk915**(npm 维护者 **ltmroberthk**),点击**安装**。以后通过市场的**更新**或**全部更新**安装符合宿主发布策略的版本。两份原生组件都已随包提供,无需 PowerShell 7、SDK 或构建白名单。
39
+
40
+ 目录尚未同步时,在 **设置 → 插件 → 添加插件** 输入修复版 **`dsh-codex-style-computer-use@1.2.1`**。正式标签提升前,省略版本号仍会装到 1.2.0。这里填 npm 包名即可;填写 GitHub 仓库网址会转为下载源码,容易受 GitHub 下载线路影响。默认非严格 pnpm 11.7.0 配置可以直接安装 1.2.1;宿主显式启用严格策略时,才可能需要等待冷却。首次新增 bundle 在官方桌面端可热加载;确认工具与 skill 已出现即可使用。替换已经加载的版本、客户端界面未刷新或界面明确提示 **restart-required / 需要重启** 时,再从托盘完整退出 DSH 并重新打开。
27
41
 
28
- 目录尚未同步时,可在 **设置 → 插件 → 添加插件** 输入 `dsh-codex-style-computer-use` 安装;这同样是可由市场更新的 npm 安装。如果界面提示**需要重启**,从托盘彻底退出 DSH 后重新打开。
42
+ ### 发布未满 24 小时
43
+
44
+ 默认非严格 pnpm 11.7.0 配置会自动记录单版本例外并完成安装;“这台能装”不能推断另一台也允许安装新版本。
45
+
46
+ 只有在 `minimumReleaseAgeStrict: true`(或宿主显式启用严格冷却)、版本未满配置的冷却时间且没有匹配例外时,才会在插件代码运行前被拒绝。已实测在该策略下,裸包名、`dsh-codex-style-computer-use@1.2.0` 和 npm 的 tgz 下载地址都会报 `ERR_PNPM_NO_MATURE_MATCHING_VERSION`。在 pnpm 11.7.0 中,显式设置 `minimumReleaseAge: 1440` 也会启用严格行为,除非另行设置 `minimumReleaseAgeStrict: false`。重启 DSH、反复重装或安装 PowerShell 都不能解决严格策略的拒绝。官方桌面桥接不接受额外 pnpm 参数。
47
+
48
+ **如果被严格策略拦截,无需改配置的做法是等待该版本满冷却时间后,再安装或更新。** 1.2.0 的 npm 发布时刻为 2026-10-03 11:50:11.586(北京时间),24 小时门槛在 **2026-10-04 11:50:12** 后满足。自定义更长冷却或镜像同步延迟仍以宿主提示为准。默认非严格配置的用户安装本包不需要等待,也不需要手动修改配置。
49
+
50
+ 若明确要在冷却期内安装,经确认后可在目标 profile 的 `pnpm-workspace.yaml` 中,将下面的**单个版本**合并进现有列表,再回到市场重试;不要覆盖原有项,也不需要把全局冷却设为 0:
51
+
52
+ ```yaml
53
+ minimumReleaseAgeExclude:
54
+ - dsh-codex-style-computer-use@1.2.0
55
+ ```
56
+
57
+ 默认 desktop 文件位于 `%USERPROFILE%\.dsh\profiles\desktop\pnpm-workspace.yaml`;设置过 `DSH_HOME` 时以实际目录为准。例外仅限这个版本,过了冷却可删除该项。
58
+
59
+ 此前文档和发布检查修正没有改变 1.2.0 运行时,因此无需重装;**1.2.1 的原生提醒修复则需要更新版本**。某些安装路径重复添加同一来源会触发宿主的 `ambiguous-install`;重复安装 1.2.0 不会得到 1.2.1 的修复。
29
60
 
30
61
  **已有本仓库旧 Git/tgz 版本:**在市场卸载旧的 **dsh-computer-use**,再安装 **dsh-codex-style-computer-use**。npm 上的无 scope 包 `dsh-computer-use` 属于另一仓库,不能用作本插件的更新。旧 Git 依赖不会因仓库改名自动迁移;完成这一次界面内迁移后,后续使用普通市场更新即可。原有 `computer_*` 工具名与 `computer-use` 设置命名空间保留。
31
62
 
63
+ **旧配置也要迁移:**先备份目标 profile 的 `cordis.patch.yml`,仅把 `id: computer-use` 覆盖项里的 `name: dsh-computer-use` 改为 `name: dsh-codex-style-computer-use`,保留整段 `config` 和 `disabled`。只保留设置命名空间并不足够:旧 name 不匹配时,DSH 会跳过覆盖项,连 `read-only`、`dryRun`、禁用状态也可能失效。不要全文替换其他插件或共享 home 层配置。可使用仓库的 `scripts/migrate-profile.mjs`:默认预览,`--apply` 才备份并原子写入,且会拒绝并发修改。
64
+
65
+ 也可下载 [配置迁移工具](https://github.com/ltmroberthk915/dsh-computer-use/releases/download/v1.2.0/computer-use-profile-migration.zip),解压后双击 `migrate-profile.cmd`。它自动寻找已安装的官方 DSH,使用 Windows 自带的 PowerShell 5.1 和 DSH 自带的 Node,修复默认 desktop profile;新包未安装或旧 bundle 仍在时会停止。它不改变发布冷却策略。自定义 profile 可通过 `migrate-profile.ps1 -ProfileDirectory <完整目录>` 预览,确认后追加 `-Apply`。
66
+
32
67
  GitHub Release 仍提供 `dsh-computer-use.tgz`,供离线或手动安装。市场自动更新优先使用 npm 包。
33
68
 
34
69
  网页版的命令行安装方式:
@@ -37,6 +72,15 @@ GitHub Release 仍提供 `dsh-computer-use.tgz`,供离线或手动安装。市
37
72
  dsh plugin --profile web add dsh-codex-style-computer-use
38
73
  ```
39
74
 
75
+ 桌面版排障必须使用**官方桌面自带的入口**。例如默认安装目录可这样查询:
76
+
77
+ ```powershell
78
+ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
79
+ & "$CuDshInstall\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop why dsh-codex-style-computer-use
80
+ ```
81
+
82
+ 若安装在 D 盘等自定义位置,先将 `$CuDshInstall` 改成桌面快捷方式实际指向的安装目录。PATH 上旧 npm CLI 的 `dsh` 可能拒绝 desktop profile;不要把 `resources/runtime/bin` 加到全局 PATH,也无需自己安装 pnpm。GUI 安装仍是首选。
83
+
40
84
  ## 随包 skill
41
85
 
42
86
  驱动类工具会被锁住,直到会话给出那句只存在于 `skills/computer-use/SKILL.md` 里的回执短语。
@@ -116,6 +160,16 @@ node mcp/src/index.js
116
160
 
117
161
  ## 开发
118
162
 
163
+ 发布者检查默认查询 npm 官方源。若本地无法访问,可用 `CU_REGISTRY` 为只读的 `check` 命令指定镜像;JSON 输出会注明 `registry` 和 `authoritative`。PowerShell 示例(只影响当前终端):
164
+
165
+ ```powershell
166
+ $env:CU_REGISTRY = 'https://registry.npmmirror.com'
167
+ node scripts/release-channel.mjs check 1.2.0
168
+ Remove-Item Env:CU_REGISTRY
169
+ ```
170
+
171
+ 退出码 `0` 表示通过发布者的 24 小时检查,`2` 表示仍在冷却,`1` 表示检查失败。网络失败不代表版本未发布,镜像也可能尚未同步。`promote` 会忽略 `CU_REGISTRY`,始终向 `https://registry.npmjs.org` 重新检查并写入 `latest`,因此需要能访问官方源,必要时使用 npm 的代理配置。本工具只供发布者使用,普通用户安装不需要运行它。详见 [发布流程](docs/RELEASE-1.2.0.md)。
172
+
119
173
  仓库自带守卫套件,保护上面这些不变量——循环生命周期、提问/刹车协议、操作分类、skill 门禁,
120
174
  以及随包 skill 的注册接线:
121
175
 
@@ -0,0 +1,39 @@
1
+ // Compatibility with hosts that conflate policy refusals and answerer refusals.
2
+ // Delegate first so the host still owns every approval and durable audit pair.
3
+ export function installApprovalAttribution (ctx, log) {
4
+ const tools = ctx.get('tools')
5
+ const original = tools?.serviceAsk
6
+ if (typeof original !== 'function') {
7
+ log.warn('approval attribution: host compatibility seam unavailable')
8
+ return
9
+ }
10
+ const ownDescriptor = Object.getOwnPropertyDescriptor(tools, 'serviceAsk')
11
+ const wrapped = async function (exec, ask) {
12
+ let policy
13
+ if (exec?.name?.startsWith('computer_') && exec.agent) {
14
+ const approval = this.ctx?.get?.('approval')
15
+ if (typeof approval?.effectivePolicy === 'function') policy = approval.effectivePolicy(exec.agent.session)
16
+ }
17
+ const result = await original.call(this, exec, ask)
18
+ // Newer/fixed hosts already provide their own reason. Only repair the exact
19
+ // known legacy attribution; never reinterpret cancellation or a grant.
20
+ if (!exec?.name?.startsWith('computer_') || result?.decision?.kind !== 'deny' ||
21
+ result.decision.reason !== `the user rejected tool "${exec.name}"`) return result
22
+ const reason = (policy === 'never'
23
+ ? `tool "${exec.name}" requires approval, but approval prompts are disabled in this session (approval policy: never); the request was automatically rejected without asking the user`
24
+ : `approval for tool "${exec.name}" was rejected by the approval channel`) +
25
+ (ask?.reason ? `; approval required because: ${ask.reason}` : '')
26
+ return { ...result, decision: { ...result.decision, reason } }
27
+ }
28
+ ctx.effect(() => {
29
+ tools.serviceAsk = wrapped
30
+ return () => {
31
+ // Cordis returns a traced callable on get(); compare the stored method,
32
+ // not that newly created callable, to identify our own replacement.
33
+ if (Object.getOwnPropertyDescriptor(tools, 'serviceAsk')?.value !== wrapped) return
34
+ if (ownDescriptor) Object.defineProperty(tools, 'serviceAsk', ownDescriptor)
35
+ else delete tools.serviceAsk
36
+ }
37
+ })
38
+ log.info('approval attribution: installed for computer-use; host approvals and audit retained')
39
+ }
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import os from 'node:os'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { spawn } from 'node:child_process'
6
+ import { randomUUID } from 'node:crypto'
6
7
  import { prepareNativeBinary } from './core/native-runtime.js'
7
8
 
8
9
  export class AttentionNative {
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { installHumanAttention } from './attention.js'
2
+ import { installApprovalAttribution } from './approval-attribution.js'
2
3
  // dsh-computer-use — DSH Cordis plugin
3
4
  //
4
5
  // Native DSH integration for computer use. Mirrors the @anweat/dsh-browser
@@ -438,6 +439,7 @@ export function apply (ctx, config) {
438
439
  return
439
440
  }
440
441
  if (!config.enabled) return
442
+ installApprovalAttribution(ctx, log)
441
443
  registerBundledSkill(ctx, log)
442
444
  raiseHuman = enableHumanAttention(ctx, log)
443
445
  // THE QUESTION SERVICE, INJECTED LAZILY (merge-design.md §5.3 item 16 — defence in depth).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-style-computer-use",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "Codex-style Computer Use for DeepSeek Harness: Windows desktop control, screenshots, UI Automation, human input handoff, approval gates and prebuilt native workers. Install and update from dsh-market without PowerShell 7 or build steps.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -0,0 +1,8 @@
1
+ @echo off
2
+ setlocal
3
+ echo Restoring saved Computer Use configuration after the package rename.
4
+ echo A backup is created before changing the desktop profile.
5
+ "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0migrate-profile.ps1" -Apply
6
+ set "CU_RESULT=%ERRORLEVEL%"
7
+ pause
8
+ exit /b %CU_RESULT%
@@ -0,0 +1,65 @@
1
+ // Explicit one-time user-patch migration. No install hook and no plugin startup write.
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { createRequire } from 'node:module'
5
+ import { fileURLToPath, pathToFileURL } from 'node:url'
6
+ import { randomUUID } from 'node:crypto'
7
+
8
+ export const OLD_NAME = 'dsh-computer-use'
9
+ export const NEW_NAME = 'dsh-codex-style-computer-use'
10
+
11
+ export function planMigration(source, yaml) {
12
+ const doc = yaml.parseDocument(source, { keepSourceTokens: true, uniqueKeys: true })
13
+ if (doc.errors.length) throw Error(`Invalid patch YAML: ${doc.errors[0].message}`)
14
+ if (!yaml.isSeq(doc.contents)) throw Error('The patch file must be a YAML list')
15
+ const replacements = [], unsupported = []
16
+ for (const item of doc.contents.items) {
17
+ if (!yaml.isMap(item) || item.get('id') !== 'computer-use') continue
18
+ const name = item.get('name', true)
19
+ if (!yaml.isScalar(name) || name.value !== OLD_NAME) continue
20
+ if (item.has('insert') || !name.range) { unsupported.push('A computer-use insert patch needs manual review'); continue }
21
+ replacements.push({ start: name.range[0], end: name.range[1], value: JSON.stringify(NEW_NAME) })
22
+ }
23
+ if (unsupported.length) throw Error(unsupported.join('; '))
24
+ let updated = source
25
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) updated = updated.slice(0, replacement.start) + replacement.value + updated.slice(replacement.end)
26
+ // Only exact name scalars are replaced; config values, disabled state, comments,
27
+ // other plugins, line endings and !!js expressions keep their original bytes.
28
+ return { source, updated, changes: replacements.length }
29
+ }
30
+
31
+ export async function applyMigration(filename, plan, atomic) {
32
+ if (!plan.changes) return { changed: false, changes: 0 }
33
+ return atomic.withFileLock(filename, async () => {
34
+ const stat = fs.lstatSync(filename)
35
+ if (!stat.isFile() || stat.isSymbolicLink()) throw Error('Refusing to replace a non-regular patch file')
36
+ if (fs.readFileSync(filename, 'utf8') !== plan.source) throw Error('The patch changed after inspection; read it again before applying')
37
+ const backup = `${filename}.before-computer-use-rename-${randomUUID()}.bak`
38
+ fs.writeFileSync(backup, plan.source, { flag: 'wx', mode: stat.mode & 0o777 })
39
+ await atomic.writeFileAtomic(filename, plan.updated, { mode: stat.mode & 0o777 })
40
+ return { changed: true, changes: plan.changes, backup }
41
+ })
42
+ }
43
+
44
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
45
+ try {
46
+ const args = process.argv.slice(2)
47
+ const value = key => { const i = args.indexOf(key); return i < 0 ? undefined : args[i + 1] }
48
+ const filename = value('--patch')
49
+ if (!filename || !path.isAbsolute(filename)) throw Error('Usage: migrate-profile.mjs --patch <absolute cordis.patch.yml> [--host-root <absolute DSH runtime>] [--apply]')
50
+ const host = value('--host-root') || path.join(path.dirname(process.execPath), 'resources/app.asar/dsh')
51
+ const require = createRequire(path.join(host, 'package.json'))
52
+ const yaml = require('yaml')
53
+ const atomic = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-atomic-write')).href)
54
+ const plan = planMigration(fs.readFileSync(filename, 'utf8'), yaml)
55
+ if (args.includes('--apply') && plan.changes) {
56
+ const profile = JSON.parse(fs.readFileSync(path.join(path.dirname(filename), 'package.json'), 'utf8'))
57
+ const bundles = profile.dsh?.profile?.bundles || []
58
+ if (!bundles.includes(NEW_NAME) || bundles.includes(OLD_NAME)) throw Error('Install the new bundle and remove the old bundle first; this tool only migrates one profile, never the shared home patch')
59
+ const installed = JSON.parse(fs.readFileSync(path.join(path.dirname(filename), 'node_modules', NEW_NAME, 'package.json'), 'utf8'))
60
+ if (installed.name !== NEW_NAME) throw Error('The new package identity does not match')
61
+ }
62
+ const result = args.includes('--apply') ? await applyMigration(filename, plan, atomic) : { changed: false, changes: plan.changes, preview: true }
63
+ console.log(JSON.stringify({ patch: filename, oldName: OLD_NAME, newName: NEW_NAME, ...result }, null, 2))
64
+ } catch (error) { console.error(error.message); process.exitCode = 1 }
65
+ }
@@ -0,0 +1,35 @@
1
+ #Requires -Version 5.1
2
+ param([switch]$Apply, [string]$DshInstall = '', [string]$ProfileDirectory = '')
3
+ $ErrorActionPreference = 'Stop'
4
+ $cuPreviousNodeMode = $env:ELECTRON_RUN_AS_NODE
5
+ try {
6
+ if (-not $DshInstall) {
7
+ $cuCandidates = @(Join-Path $env:LOCALAPPDATA 'Programs/DeepSeek Harness')
8
+ foreach ($cuRegistry in @('HKCU:/Software/Microsoft/Windows/CurrentVersion/Uninstall/*', 'HKLM:/Software/Microsoft/Windows/CurrentVersion/Uninstall/*')) {
9
+ $cuEntries = Get-ItemProperty $cuRegistry -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like 'DeepSeek Harness*' }
10
+ foreach ($cuEntry in $cuEntries) { if ($cuEntry.InstallLocation) { $cuCandidates += [string]$cuEntry.InstallLocation } }
11
+ }
12
+ $DshInstall = $cuCandidates | Where-Object { Test-Path -LiteralPath (Join-Path $_ 'DeepSeek Harness.exe') } | Select-Object -First 1
13
+ }
14
+ if (-not $DshInstall) { throw 'DSH Desktop was not found. Pass -DshInstall with its installation directory.' }
15
+ if (-not $ProfileDirectory) {
16
+ $cuDshDirectory = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
17
+ $ProfileDirectory = Join-Path $cuDshDirectory 'profiles/desktop'
18
+ }
19
+ $cuPatch = [IO.Path]::GetFullPath((Join-Path $ProfileDirectory 'cordis.patch.yml'))
20
+ if (-not (Test-Path -LiteralPath $cuPatch)) { throw "Profile patch not found: $cuPatch" }
21
+ $cuExecutable = Join-Path $DshInstall 'DeepSeek Harness.exe'
22
+ $cuScript = Join-Path $PSScriptRoot 'migrate-profile.mjs'
23
+ $cuArguments = @('--expose-internals', $cuScript, '--patch', $cuPatch)
24
+ if ($Apply) { $cuArguments += '--apply' }
25
+ $env:ELECTRON_RUN_AS_NODE = '1'
26
+ # Piping makes Windows PowerShell wait for Electron's GUI-subsystem exe.
27
+ & $cuExecutable @cuArguments | Out-Host
28
+ if ($LASTEXITCODE -ne 0) { throw "Migration failed (exit $LASTEXITCODE). No success is claimed; inspect the message above." }
29
+ } catch {
30
+ Write-Host $_.Exception.Message -ForegroundColor Red
31
+ exit 1
32
+ } finally {
33
+ if ($null -eq $cuPreviousNodeMode) { Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue }
34
+ else { $env:ELECTRON_RUN_AS_NODE = $cuPreviousNodeMode }
35
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createHash } from 'node:crypto'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const automaticReason = 'the request was automatically rejected without asking the user'
7
+ const channelReason = 'was rejected by the approval channel'
8
+
9
+ function replaceOnce (source, before, after) {
10
+ if (source.split(before).length !== 2) throw new Error('Unsupported DSH tools source: missing or ambiguous patch anchor')
11
+ return source.replace(before, after)
12
+ }
13
+
14
+ /** Patch the compiled DSH 0.2.1-rc.2 tool runtime; policy and audit stay unchanged. */
15
+ export function patchApprovalAttribution (source) {
16
+ const start = source.indexOf('\tasync serviceAsk(exec, ask) {')
17
+ const end = source.indexOf('\n\t/**', start)
18
+ if (start < 0 || end < start) throw new Error('Unsupported DSH tools source: serviceAsk not found')
19
+ const method = source.slice(start, end)
20
+ const originalReason = '\t\t\t\t\treason: `the user rejected tool "${exec.name}"`'
21
+ if (method.includes(automaticReason) && method.includes(channelReason) &&
22
+ method.includes('const approvalPolicy = approval.effectivePolicy(exec.agent.session);') &&
23
+ !method.includes(originalReason)) return { source, changed: false }
24
+ let patched = replaceOnce(method,
25
+ '\t\tconst outcome = await approval.request({',
26
+ '\t\tconst approvalPolicy = approval.effectivePolicy(exec.agent.session);\n\t\tconst outcome = await approval.request({')
27
+ patched = replaceOnce(patched, originalReason,
28
+ '\t\t\t\t\treason: (approvalPolicy === "never"\n' +
29
+ '\t\t\t\t\t\t? `tool "${exec.name}" requires approval, but approval prompts are disabled in this session (approval policy: never); the request was automatically rejected without asking the user`\n' +
30
+ '\t\t\t\t\t\t: `approval for tool "${exec.name}" was rejected by the approval channel`) + (ask.reason ? `; approval required because: ${ask.reason}` : "")')
31
+ // Replace the obsolete attribution comment; this also keeps the verified
32
+ // compiled module small enough for a separate fixed-layout ASAR deployment.
33
+ const docStart = source.lastIndexOf('\t/**\n\t* Resolve an `ask` decision', start)
34
+ if (docStart < 0) throw new Error('Unsupported DSH tools source: expected serviceAsk documentation')
35
+ const doc = '\t/**\n\t* Resolve asks through approval, retaining fail-closed behavior.\n\t* Snapshot policy before requesting: the never policy skips all answerers.\n\t* A channel rejection alone does not identify a human as its source.\n\t*/\n'
36
+ return { source: source.slice(0, docStart) + doc + patched + source.slice(end), changed: true }
37
+ }
38
+
39
+ /** Preserve the original UTF-8 byte length, using trailing whitespace only. */
40
+ export function fixedLengthPayload (original, patched) {
41
+ const size = Buffer.byteLength(original)
42
+ const content = Buffer.from(patched)
43
+ if (content.length > size) throw new Error('Patched source exceeds the original byte length')
44
+ const output = Buffer.alloc(size, 0x20)
45
+ content.copy(output)
46
+ if (content.length < size) output[output.length - 1] = 0x0a
47
+ return output
48
+ }
49
+
50
+ function main (args) {
51
+ let input, output, fixedLength = false
52
+ for (let i = 0; i < args.length; i++) {
53
+ if (args[i] === '--input' || args[i] === '--output') {
54
+ const option = args[i], value = args[++i]
55
+ if (!value || value.startsWith('--')) throw new Error(`Missing value for ${option}`)
56
+ if (option === '--input') {
57
+ if (input) throw new Error('Duplicate --input')
58
+ input = path.resolve(value)
59
+ } else {
60
+ if (output) throw new Error('Duplicate --output')
61
+ output = path.resolve(value)
62
+ }
63
+ } else if (args[i] === '--fixed-length') fixedLength = true
64
+ else throw new Error(`Unknown option: ${args[i]}`)
65
+ }
66
+ if (!input) throw new Error('Usage: node scripts/patch-approval-attribution.mjs --input <extracted dsh-tools/lib/index.js> [--output <new file>] [--fixed-length]')
67
+ if (output === input) throw new Error('--output must be a new file; installation replacement is a separate step')
68
+ const original = fs.readFileSync(input, 'utf8')
69
+ const patch = patchApprovalAttribution(original)
70
+ const payload = fixedLength ? fixedLengthPayload(original, patch.source) : Buffer.from(patch.source)
71
+ if (output) fs.writeFileSync(output, payload, { flag: 'wx' })
72
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex')
73
+ console.log(JSON.stringify({
74
+ status: patch.changed ? 'patched' : 'already-patched',
75
+ preview: !output,
76
+ originalBytes: Buffer.byteLength(original), patchedBytes: payload.length,
77
+ originalSha256: hash(Buffer.from(original)), patchedSha256: hash(payload),
78
+ }, null, 2))
79
+ }
80
+
81
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
82
+ try { main(process.argv.slice(2)) } catch (error) {
83
+ console.error(error.message)
84
+ process.exitCode = 1
85
+ }
86
+ }
@@ -0,0 +1,78 @@
1
+ // Publisher tool. This file is never an npm lifecycle hook.
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { execFileSync } from 'node:child_process'
6
+
7
+ export const PACKAGE = 'dsh-codex-style-computer-use'
8
+ export const MIN_AGE_MS = 24 * 60 * 60 * 1000
9
+ export const OFFICIAL_REGISTRY = 'https://registry.npmjs.org'
10
+
11
+ // A mirror is useful for a read-only local check. Promotion must recheck and
12
+ // write the authoritative registry, even when CU_REGISTRY remains in the shell.
13
+ export function releaseRegistry(operation, override = process.env.CU_REGISTRY) {
14
+ if (operation === 'promote') return OFFICIAL_REGISTRY
15
+ if (operation !== 'check') throw Error('Expected check or promote')
16
+ let registry
17
+ try { registry = new URL(override?.trim() || OFFICIAL_REGISTRY) }
18
+ catch { throw Error('CU_REGISTRY must be an HTTPS registry URL') }
19
+ if (registry.protocol !== 'https:' || registry.username || registry.password || registry.search || registry.hash) {
20
+ throw Error('CU_REGISTRY must be an HTTPS registry URL without credentials, query or fragment')
21
+ }
22
+ return registry.href.replace(/\/+$/, '')
23
+ }
24
+
25
+ export function releaseReadiness(metadata, version, now = Date.now()) {
26
+ if (!/^\d+\.\d+\.\d+$/.test(version)) throw Error('Promotion requires an exact stable version, e.g. 1.2.1')
27
+ const manifest = metadata.versions?.[version]
28
+ if (manifest?.name !== PACKAGE || manifest.version !== version) throw Error('The requested published version is missing or has the wrong identity')
29
+ const repository = typeof manifest.repository === 'string' ? manifest.repository : manifest.repository?.url
30
+ if (repository?.replace(/^git\+/, '').replace(/\.git$/, '').replace(/\/$/, '') !== 'https://github.com/ltmroberthk915/dsh-computer-use') throw Error('Published repository identity does not match')
31
+ const publishedText = metadata.time?.[version]
32
+ const publishedAt = typeof publishedText === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(publishedText) ? Date.parse(publishedText) : NaN
33
+ if (!Number.isFinite(publishedAt) || publishedAt > now) throw Error('Missing or invalid registry publication time; refusing promotion')
34
+ const latest = metadata['dist-tags']?.latest
35
+ if (latest && /^\d+\.\d+\.\d+$/.test(latest)) {
36
+ const a = version.split('.').map(Number), b = latest.split('.').map(Number)
37
+ for (let i = 0; i < 3; i++) {
38
+ if (a[i] < b[i]) throw Error('Refusing to move latest backwards')
39
+ if (a[i] > b[i]) break
40
+ }
41
+ }
42
+ const eligibleAt = publishedAt + MIN_AGE_MS
43
+ return { package: PACKAGE, version, ready: now >= eligibleAt, publishedAt: new Date(publishedAt).toISOString(), eligibleAt: new Date(eligibleAt).toISOString(), remainingSeconds: Math.max(0, Math.ceil((eligibleAt - now) / 1000)) }
44
+ }
45
+
46
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
47
+ const [operation, version] = process.argv.slice(2)
48
+ let registry
49
+ try {
50
+ if (!['check', 'promote'].includes(operation) || !/^\d+\.\d+\.\d+$/.test(version || '')) throw Error('Usage: node scripts/release-channel.mjs check|promote <stable-version>')
51
+ registry = releaseRegistry(operation)
52
+ const npm = path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js')
53
+ if (!fs.existsSync(npm)) throw Error('Run this publisher command with the Node.js installation that includes npm')
54
+ const npmArgs = [`--registry=${registry}`, '--fetch-retries=0', '--fetch-timeout=20000']
55
+ const view = args => {
56
+ let stdout
57
+ try {
58
+ stdout = execFileSync(process.execPath, [npm, 'view', ...args, '--json', ...npmArgs], { encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
59
+ } catch (error) {
60
+ const code = String(error.stderr ?? '').match(/npm (?:error|ERR!) code ([A-Z0-9_]+)/i)?.[1] || error.code || error.status || 'unknown'
61
+ throw Error(`Registry query failed (${code}). Check network/proxy access and mirror synchronization; a failed query does not prove the package is unpublished. For a read-only check, set CU_REGISTRY to an accessible mirror. Promotion requires access to registry.npmjs.org.`)
62
+ }
63
+ try { return JSON.parse(stdout) }
64
+ catch { throw Error('Registry query did not return valid JSON; release readiness is unknown') }
65
+ }
66
+ const manifest = view([`${PACKAGE}@${version}`])
67
+ const metadata = { ...manifest, versions: { [version]: manifest } }
68
+ const verdict = { ...releaseReadiness(metadata, version), registry, authoritative: registry === OFFICIAL_REGISTRY }
69
+ console.log(JSON.stringify(verdict, null, 2))
70
+ if (!verdict.ready) { process.exitCode = 2 }
71
+ else if (operation === 'promote') {
72
+ execFileSync(process.execPath, [npm, 'dist-tag', 'add', `${PACKAGE}@${version}`, 'latest', ...npmArgs], { stdio: 'inherit', windowsHide: true })
73
+ const tag = view([PACKAGE, 'dist-tags.latest'])
74
+ if (tag !== version) throw Error('Registry latest did not match after promotion')
75
+ console.log(`Verified latest: ${PACKAGE}@${version}`)
76
+ }
77
+ } catch (error) { console.error(JSON.stringify({ operation, registry, error: error.message }, null, 2)); process.exitCode = 1 }
78
+ }
@@ -23,10 +23,14 @@ try {
23
23
  assert.equal(typeof cursor.x, 'number')
24
24
  const shot = await cu.screenshot({ maxWidth: 320 })
25
25
  assert(shot.image.length > 100)
26
- attention = new AttentionNative({ dataDir: path.join(scratch, 'attention') })
26
+ attention = new AttentionNative({ exe: path.join(scratch, 'absent-host.exe'), hostPid: 0, dataDir: path.join(scratch, 'attention') })
27
27
  assert.match(await attention.compile(), /prebuilt/)
28
28
  await attention.start()
29
- console.log(JSON.stringify({ ok: true, pathEmpty: true, coldCache: true, prebuiltWorker: true, ping: true, screenshotBytes: shot.image.length, attentionReady: true }))
29
+ const probe = await attention.request('probe')
30
+ assert.equal(probe.status, 'no-window')
31
+ assert.equal(probe.visible, false)
32
+ assert.match(probe.requestId, /^[0-9a-f-]{36}$/i)
33
+ console.log(JSON.stringify({ ok: true, pathEmpty: true, coldCache: true, prebuiltWorker: true, ping: true, screenshotBytes: shot.image.length, attentionReady: true, attentionProbe: true }))
30
34
  } finally {
31
35
  await attention?.close()
32
36
  await cu?.kill()
@@ -22,7 +22,7 @@ for (const [source, binary] of [
22
22
  assert.equal(manifest.platform, 'win32')
23
23
  assert.equal(manifest.architecture, 'x64')
24
24
  }
25
- for (const file of ['lib/index.js', 'lib/core/native-runtime.js', 'lib/attention-native.js', 'skills/computer-use/SKILL.md', 'mcp/src/index.js']) assert(fs.existsSync(path.join(root, file)), file)
25
+ for (const file of ['lib/index.js', 'lib/approval-attribution.js', 'lib/core/native-runtime.js', 'lib/attention-native.js', 'skills/computer-use/SKILL.md', 'mcp/src/index.js']) assert(fs.existsSync(path.join(root, file)), file)
26
26
  const attention = fs.readFileSync(path.join(root, 'lib/attention-native.js'), 'utf8')
27
27
  assert(attention.includes("'./core/native-runtime.js'"), 'attention helper must use the bundled core')
28
28
  console.log(`ok package: ${pkg.name}@${pkg.version}; prebuilt helpers verified; no install hooks`)