@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/README.zh.md +195 -0
  4. package/assets/icon.svg +47 -0
  5. package/cordis.patch.yml +41 -0
  6. package/lib/bridge/acp-bridge.js +905 -0
  7. package/lib/bridge/peer-connection.js +730 -0
  8. package/lib/client.js +2842 -0
  9. package/lib/core/acp-model-overlay.js +121 -0
  10. package/lib/core/approval-queue.js +216 -0
  11. package/lib/core/audit-log.js +322 -0
  12. package/lib/core/desktop-notify.js +173 -0
  13. package/lib/core/environment-prompt.js +76 -0
  14. package/lib/core/relay-supervisor.js +278 -0
  15. package/lib/core/review-engine.js +679 -0
  16. package/lib/core/sandbox-diagnosis.js +258 -0
  17. package/lib/core/sandbox-probe.js +310 -0
  18. package/lib/core/self-acp-spec.js +117 -0
  19. package/lib/core/selfcheck-runner.js +150 -0
  20. package/lib/core/settings-store.js +226 -0
  21. package/lib/core/shot-gc.js +334 -0
  22. package/lib/core/task-cwd.js +50 -0
  23. package/lib/core/task-memory.js +163 -0
  24. package/lib/core/task-registry.js +238 -0
  25. package/lib/core/toolcall-cache.js +206 -0
  26. package/lib/core/transfer-queue.js +58 -0
  27. package/lib/core/transfer.js +1144 -0
  28. package/lib/core/worker-arming.js +334 -0
  29. package/lib/index.js +2207 -0
  30. package/lib/provider/out-of-process-helpers.js +76 -0
  31. package/lib/provider/remote-provider.js +261 -0
  32. package/lib/tools/mediator-shim.mjs +180 -0
  33. package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
  34. package/lib/tools/remote-tools.js +680 -0
  35. package/lib/tools/selfcheck-fix-prompt.md +206 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +72 -4
  39. package/slave/acp-profile-approval.patch.yml +66 -0
  40. package/slave/file-transfer.mjs +552 -0
  41. package/slave/integrity-probe.ps1 +99 -0
  42. package/slave/make-launch-spec.mjs +184 -0
  43. package/slave/net-address.mjs +138 -0
  44. package/slave/node_modules/.package-lock.json +26 -0
  45. package/slave/node_modules/ws/LICENSE +20 -0
  46. package/slave/node_modules/ws/README.md +548 -0
  47. package/slave/node_modules/ws/browser.js +8 -0
  48. package/slave/node_modules/ws/index.js +22 -0
  49. package/slave/node_modules/ws/lib/buffer-util.js +131 -0
  50. package/slave/node_modules/ws/lib/constants.js +19 -0
  51. package/slave/node_modules/ws/lib/event-target.js +292 -0
  52. package/slave/node_modules/ws/lib/extension.js +203 -0
  53. package/slave/node_modules/ws/lib/limiter.js +55 -0
  54. package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
  55. package/slave/node_modules/ws/lib/receiver.js +743 -0
  56. package/slave/node_modules/ws/lib/sender.js +607 -0
  57. package/slave/node_modules/ws/lib/stream.js +161 -0
  58. package/slave/node_modules/ws/lib/subprotocol.js +62 -0
  59. package/slave/node_modules/ws/lib/validation.js +152 -0
  60. package/slave/node_modules/ws/lib/websocket-server.js +562 -0
  61. package/slave/node_modules/ws/lib/websocket.js +1421 -0
  62. package/slave/node_modules/ws/package.json +74 -0
  63. package/slave/node_modules/ws/wrapper.mjs +21 -0
  64. package/slave/pairing.mjs +175 -0
  65. package/slave/permission-pairing.mjs +240 -0
  66. package/slave/prune-shots.mjs +170 -0
  67. package/slave/relay.mjs +1351 -0
  68. package/slave/repair-workspace-integrity.ps1 +75 -0
  69. package/slave/screenshot.ps1 +114 -0
  70. package/slave/selfcheck.ps1 +350 -0
  71. package/slave/stage-app.ps1 +116 -0
  72. package/slave/workspace-integrity.mjs +157 -0
@@ -0,0 +1,206 @@
1
+ # 从机环境自动配置提示词(复制给从机的 DSH 会话)
2
+
3
+ > 用法:在**从机**的 DSH 会话里新建一个对话,把本文件从「你要做什么」往下整段粘进去。
4
+ > 它让从机自己的 agent 去跑自检、按项修复、再复跑证明。需要管理员权限或需要人登录/解锁的步骤,
5
+ > 它会停下来告诉你,而不是假装成功。
6
+
7
+ ---
8
+
9
+ ## 你要做什么
10
+
11
+ 这台 Windows 机器是 DSH 的**远程工作节点**。请实际执行下面的清单,把它的 GUI 就绪环境配好,并用自检脚本
12
+ **证明**每一项都过了。不要猜测;每一步都贴**原始输出**;做不到的项明确写"做不到,原因/需要什么"。
13
+
14
+ ## 纪律
15
+
16
+ - 只在这台机器上操作;**不要修改 DSH 安装目录,也不要手改任何 profile**。
17
+ - 需要管理员权限的步骤先说明再执行;安装驱动/软件前先征得操作者同意。
18
+ - 装驱动只用官方发布(Virtual-Display-Driver 是 MIT);**不要**为了"让检查过"而关 UAC / 防火墙 / 安全策略。
19
+ - 最后必须**复跑自检并贴 JSON**。"命令返回 0" 不等于 "检查项通过"。
20
+
21
+ ## 第 0 步:找到并运行自检
22
+
23
+ ```powershell
24
+ $selfcheck = Get-ChildItem "$env:USERPROFILE\.dsh\profiles\*\node_modules\@hope_phenom\dsh-plugin-remote-use\slave\selfcheck.ps1" -ErrorAction SilentlyContinue | Select-Object -First 1
25
+ if ($null -eq $selfcheck) { throw "找不到 selfcheck.ps1:这台机器还没装 @hope_phenom/dsh-plugin-remote-use" }
26
+ $ps = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
27
+ & $ps -NoProfile -ExecutionPolicy Bypass -File $selfcheck.FullName
28
+ Get-Content (Join-Path $selfcheck.DirectoryName "selfcheck.json") -Raw
29
+ ```
30
+
31
+ - 自检必须在**已登录、已解锁的交互式桌面会话**里跑(就是 DSH 桌面进程所在的那个会话)。
32
+ SSH / 计划任务 / 服务里跑会得到 `session=0` 的假阴性(S1 显示 0 块屏、S6/S7/S8 n/a),那些不是真问题。
33
+ - 布防时填了任务目录的话,加 `-Workspace "<任务目录>"`(它会顺带测量 S20 工作目录完整性)。
34
+ - 把输出的 `verdict`、`blockingFailures` 和 `checks[]`(每项的 `id/status/actual/fix`)都留着,后面要对比。
35
+
36
+ ## 第 1 步:按报告逐项修复
37
+
38
+ > 先修 **blocking** 项,再修 warning。每修完一类就复跑一次自检,确认那一项真的变了。
39
+
40
+ ### S1 / S2 显示适配器与虚拟显示器(blocking)
41
+
42
+ 判据:至少一个 `ATTACHED_TO_DESKTOP` 的适配器;并且存在**带 EDID 的虚拟显示器**,不能回落到 `WinDisc`
43
+ (Microsoft 基本显示适配器,通常 1024x768)。
44
+
45
+ ```powershell
46
+ Get-PnpDevice -Class Display | Select-Object Status, Problem, FriendlyName, InstanceId | Format-List
47
+ Add-Type -AssemblyName System.Windows.Forms
48
+ [System.Windows.Forms.Screen]::AllScreens | ForEach-Object { "$($_.DeviceName) $($_.Bounds.Width)x$($_.Bounds.Height) at $($_.Bounds.X),$($_.Bounds.Y) primary=$($_.Primary)" }
49
+ ```
50
+
51
+ - 如果 VDD 已装但被禁用(`Problem=CM_PROB_DISABLED` / Code 22):
52
+ `Enable-PnpDevice -InstanceId 'ROOT\DISPLAY\0000' -Confirm:$false`(需要管理员)。
53
+ - 如果根本没装 VDD:按官方 **Virtual-Display-Driver(VDD,MIT)** 的发布说明安装(需要管理员,通常要重启)。
54
+ **先向操作者确认**再装;装完再跑一次自检,S2 的 `actual` 里应出现 VDD 与 EDID,而不是 `WinDisc`。
55
+ - ⚠️ **不要把虚拟屏设成主屏**:Windows 的登录界面只画在主屏上,主屏若是没有物理输出的虚拟屏,
56
+ 操作者会看到黑屏、无法登录。主屏保持物理屏,虚拟屏作为**第二块**。
57
+
58
+ ### S3 / S4 分辨率与原点(S3 警告,S4 阻断)
59
+
60
+ - **S4 是阻断项**:主屏必须在 `(0,0)`,否则 GUI 自动化的绝对坐标会整体偏移(而且不报错)。
61
+ - S3 期望主屏 `1920x1080`;物理主屏做不到时它只是警告,不用强求。
62
+
63
+ 下面的脚本会把**主屏**设成 1920x1080@(0,0),并(可选)把第二块屏设成 1920x1080@(1920,0):
64
+
65
+ ```powershell
66
+ Add-Type @"
67
+ using System;
68
+ using System.Runtime.InteropServices;
69
+ public class DispCfg {
70
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
71
+ public struct DEVMODE {
72
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName;
73
+ public short dmSpecVersion; public short dmDriverVersion; public short dmSize; public short dmDriverExtra;
74
+ public int dmFields;
75
+ public int dmPositionX; public int dmPositionY; public int dmDisplayOrientation; public int dmDisplayFixedOutput;
76
+ public short dmColor; public short dmDuplex; public short dmYResolution; public short dmTTOption; public short dmCollate;
77
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName;
78
+ public short dmLogPixels;
79
+ public int dmBitsPerPel; public int dmPelsWidth; public int dmPelsHeight;
80
+ public int dmDisplayFlags; public int dmDisplayFrequency;
81
+ public int dmICMMethod; public int dmICMIntent; public int dmMediaType; public int dmDitherType;
82
+ public int dmReserved1; public int dmReserved2; public int dmPanningWidth; public int dmPanningHeight;
83
+ }
84
+ [DllImport("user32.dll", CharSet = CharSet.Ansi)] public static extern bool EnumDisplaySettings(string dev, int mode, ref DEVMODE dm);
85
+ [DllImport("user32.dll", CharSet = CharSet.Ansi)] public static extern int ChangeDisplaySettingsEx(string dev, ref DEVMODE dm, IntPtr hwnd, int flags, IntPtr param);
86
+ public const int ENUM_CURRENT_SETTINGS = -1;
87
+ public const int CDS_UPDATEREGISTRY = 0x01;
88
+ public const int DM_POSITION = 0x20, DM_PELSWIDTH = 0x80000, DM_PELSHEIGHT = 0x100000, DM_DISPLAYFREQUENCY = 0x400000;
89
+ }
90
+ "@
91
+
92
+ Add-Type -AssemblyName System.Windows.Forms
93
+ $displays = @([System.Windows.Forms.Screen]::AllScreens)
94
+
95
+ function Set-DisplayMode([string]$dev, [int]$w, [int]$h, [int]$x, [int]$y, [int]$hz) {
96
+ $dm = New-Object DispCfg+DEVMODE
97
+ $dm.dmSize = [short][Runtime.InteropServices.Marshal]::SizeOf($dm)
98
+ if (-not [DispCfg]::EnumDisplaySettings($dev, -1, [ref]$dm)) { return "READ-FAILED $dev" }
99
+ $dm.dmPelsWidth = $w; $dm.dmPelsHeight = $h
100
+ $dm.dmPositionX = $x; $dm.dmPositionY = $y
101
+ $dm.dmDisplayFrequency = $hz
102
+ $dm.dmFields = [DispCfg]::DM_PELSWIDTH -bor [DispCfg]::DM_PELSHEIGHT -bor [DispCfg]::DM_POSITION -bor [DispCfg]::DM_DISPLAYFREQUENCY
103
+ $rc = [DispCfg]::ChangeDisplaySettingsEx($dev, [ref]$dm, [IntPtr]::Zero, [DispCfg]::CDS_UPDATEREGISTRY, [IntPtr]::Zero)
104
+ return "rc=$rc $dev ${w}x${h}@${x},${y} ${hz}Hz"
105
+ }
106
+
107
+ $primary = $displays | Where-Object { $_.Primary } | Select-Object -First 1
108
+ Set-DisplayMode $primary.DeviceName 1920 1080 0 0 60
109
+ # 可选:把第二块屏排到主屏右侧,同样 1920x1080
110
+ $others = @($displays | Where-Object { -not $_.Primary })
111
+ if ($others.Count -gt 0) { Set-DisplayMode $others[0].DeviceName 1920 1080 1920 0 60 }
112
+ Start-Sleep -Seconds 2
113
+ [System.Windows.Forms.Screen]::AllScreens | ForEach-Object { "$($_.DeviceName) $($_.Bounds.Width)x$($_.Bounds.Height) at $($_.Bounds.X),$($_.Bounds.Y) primary=$($_.Primary)" }
114
+ ```
115
+
116
+ 返回码含义:`0` 成功、`1` 需要重启、`-1` 失败、`-2` 不支持该模式。失败就贴返回码,**不要换别的分辨率硬试**。
117
+ (如果该屏不支持 1920x1080,S3 保持警告即可;S4 必须满足。)
118
+
119
+ ### S6 / S7 / S8 交互式会话(S6/S7/S8)
120
+
121
+ 判据:自检运行在 **console 会话**里,且该会话**已登录、未锁屏**;S7/S8 需要真的能动鼠标(S8 会移动光标)。
122
+
123
+ - 这三项**不是 agent 远程能修的**:锁屏/未登录时必须由操作者在这台机器上(或 RDP)登录并解锁。
124
+ - 先用 `query session` 确认有一个 `Active` 的 console 会话;S6 会告诉你 `mySession` 与 `consoleId` 是否一致。
125
+ - 如果自检是在 SSH 里跑的,换到 DSH 桌面会话里重跑(见第 0 步)。
126
+
127
+ ### S9 睡眠/休眠超时(blocking)
128
+
129
+ ```powershell
130
+ powercfg /change standby-timeout-ac 0
131
+ powercfg /change standby-timeout-dc 0
132
+ powercfg /change hibernate-timeout-ac 0
133
+ powercfg /change hibernate-timeout-dc 0
134
+ powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE
135
+ ```
136
+
137
+ 可能需要管理员。目标是 AC/DC 的超时都变成 `0`。
138
+
139
+ ### S10 屏保(warning)
140
+
141
+ ```powershell
142
+ $k = "HKCU:\Control Panel\Desktop"
143
+ Set-ItemProperty $k -Name ScreenSaveActive -Value "0" -Type String
144
+ Set-ItemProperty $k -Name ScreenSaveTimeOut -Value "0" -Type String
145
+ Set-ItemProperty $k -Name ScreenSaverIsSecure -Value "0" -Type String
146
+ ```
147
+
148
+ ### S11 自动锁屏策略(blocking)
149
+
150
+ ```powershell
151
+ $k = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
152
+ if (Test-Path $k) { Remove-ItemProperty $k -Name InactivityTimeoutSecs -ErrorAction SilentlyContinue }
153
+ ```
154
+
155
+ 需要管理员;删完注销/重启才彻底生效。
156
+
157
+ ### S12 控制台 QuickEdit(blocking)
158
+
159
+ `QuickEdit` 会让控制台在选中文字时**挂起输出**,GUI 任务里表现为"命令卡住"。对当前用户和默认控制台都关掉:
160
+
161
+ ```powershell
162
+ Set-ItemProperty "HKCU:\Console" -Name QuickEdit -Value 0 -Type DWord
163
+ Get-ChildItem "HKCU:\Console" -ErrorAction SilentlyContinue | ForEach-Object {
164
+ Set-ItemProperty $_.PSPath -Name QuickEdit -Value 0 -Type DWord -ErrorAction SilentlyContinue
165
+ }
166
+ ```
167
+
168
+ **新开的控制台**才会读到新值(中继/自检下次启动时生效)。
169
+
170
+ ### S14 / S15 DSH 与模型凭据(blocking)
171
+
172
+ - S14:DSH 已安装(`actual` 会给出安装版本)。
173
+ - S15:`%USERPROFILE%\.dsh\.credentials.yaml` 存在。**缺凭据只能由人登录 DSH 解决**(agent 不能替登录)。
174
+
175
+ ### S16 磁盘空间(blocking)
176
+
177
+ 自检要求 ≥ 50 GB 可用。清理临时目录/旧截图(`%TEMP%\dsh-plugin-remote-use\shots` 等),或在布防面板把
178
+ 截图缓存上限设小。
179
+
180
+ ### S18 被测应用(warning,可选)
181
+
182
+ 只有在需要从机验证某个具体程序时才配:给自检传 `-App "<exe 路径>"`,或设置 `DSH_SELFCHECK_APP`。
183
+ 没配就是"不验证具体应用",不影响 GUI 任务。
184
+
185
+ ### S20 工作目录完整性(warning)
186
+
187
+ `danger-full-access` 下中继启动时会自动修复任务目录的 Low 完整性标签。需要手工修时:
188
+
189
+ ```powershell
190
+ powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $selfcheck.DirectoryName "repair-workspace-integrity.ps1") -Path "<任务目录>"
191
+ ```
192
+
193
+ ## 第 2 步:复跑并汇报
194
+
195
+ 再跑一次第 0 步(带 `-Workspace`),然后把**修复前/修复后**两份 `verdict + checks` 贴出来,逐项说明:
196
+
197
+ - 哪些从 fail → pass;
198
+ - 哪些仍是 fail,原因是什么、需要操作者做什么(登录/解锁、管理员、装驱动、重启);
199
+ - **不要**把"我改了注册表"当成"检查通过"——以复跑结果为准。
200
+
201
+ ## 不要做的事
202
+
203
+ - 不要把虚拟屏设成主屏(登录界面会黑屏)。
204
+ - 不要关 UAC / 防火墙 / Defender 来"让检查通过"。
205
+ - 不要在 SSH/服务会话里下结论(你会看到 session=0 的假阴性)。
206
+ - 不要改 DSH 的安装目录或 profile 文件;本清单只动操作系统设置。
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Remote Worker (LAN)",
4
+ "description": "Delegate tasks to a paired LAN worker and drive it like a subagent over ACP."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "远程工作节点(局域网)",
4
+ "description": "把任务委托给已配对的局域网从机,并用 ACP 像调用子 Agent 一样驱动它。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@hope_phenom/dsh-plugin-remote-use",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.24",
4
+ "description": "Delegate DSH tasks to a paired LAN worker and drive it like a subagent (ACP over WebSocket).",
5
+ "license": "MIT",
6
+ "author": "Hope.Phenom <hope.phenom@foxmail.com>",
7
+ "homepage": "https://github.com/Hope-Phenom/dsh-plugin-remote-use#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Hope-Phenom/dsh-plugin-remote-use.git",
11
+ "directory": "packages/dsh-plugin-remote-use"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Hope-Phenom/dsh-plugin-remote-use/issues"
15
+ },
16
+ "keywords": [
17
+ "dsh",
18
+ "dsh-plugin",
19
+ "acp",
20
+ "agent-client-protocol",
21
+ "subagent",
22
+ "remote",
23
+ "lan",
24
+ "gui-automation"
25
+ ],
26
+ "type": "module",
27
+ "main": "lib/index.js",
28
+ "icon": "./assets/icon.svg",
29
+ "exports": {
30
+ ".": "./lib/index.js",
31
+ "./client": "./lib/client.js",
32
+ "./package.json": "./package.json",
33
+ "./locale/*.json": "./locale/*.json"
34
+ },
35
+ "files": [
36
+ "lib",
37
+ "assets",
38
+ "locale",
39
+ "cordis.patch.yml",
40
+ "README.md",
41
+ "README.zh.md",
42
+ "slave/*.mjs",
43
+ "slave/*.ps1",
44
+ "slave/acp-profile-approval.patch.yml",
45
+ "slave/node_modules"
46
+ ],
47
+ "scripts": {
48
+ "test": "node --test"
49
+ },
50
+ "dsh": {
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ },
54
+ "client": {
55
+ "platform": "web"
56
+ }
57
+ },
58
+ "dependencies": {
59
+ "@agentclientprotocol/sdk": "1.4.0",
60
+ "ws": "8.22.0",
61
+ "zod": "4.6.5"
62
+ },
63
+ "peerDependencies": {
64
+ "@deepseek-ai/schemastery": "*"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@deepseek-ai/schemastery": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "engines": {
72
+ "node": ">=20"
73
+ }
74
+ }
@@ -0,0 +1,66 @@
1
+ # Worker-side ACP profile patch: the approval row, made safe for a full-access deployment.
2
+ #
3
+ # WHERE THIS GOES: `%DSH_HOME%\profiles\acp\cordis.patch.yml` on the worker, as the entry for the
4
+ # `approval` row. The rest of that file is machine-specific (model pin, sandbox mediator paths) and
5
+ # is not reproduced here -- only the one entry that decides whether full access is SAFE to set.
6
+ #
7
+ # WHEN YOU NEED IT: when this worker's launch spec is generated with
8
+ # node make-launch-spec.mjs --permission-mode danger-full-access
9
+ # i.e. when the machine is a DEDICATED GUI worker. That is the intended setup for GUI automation
10
+ # (see DESIGN section 8.6.3 and PITFALLS F33): a per-call escalation cannot host a multi-step GUI
11
+ # session, so the permission level is a property of the deployment, not of each task.
12
+ #
13
+ # ---------------------------------------------------------------------------
14
+ # WHY THIS EXISTS (PITFALLS F34 -- read it before changing this)
15
+ #
16
+ # `dsh-base` derives BOTH the sandbox mode and the approval policy from ONE environment variable:
17
+ #
18
+ # cordis.patch.yml:232 sandbox-policy.mode = process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
19
+ # cordis.patch.yml:248 approval.policy = (that value === 'danger-full-access') ? 'never' : 'ask'
20
+ #
21
+ # So DSH pairs "full-access sandbox" with "approval: never" -- that pairing IS the `danger-full-access`
22
+ # PRESET. The `permission` row (`@deepseek-ai/dsh-permission-presets`) is composed with a fixed table:
23
+ #
24
+ # read-only -> sandbox: read-only, approval: ask
25
+ # workspace-write -> sandbox: workspace-write, approval: ask
26
+ # danger-full-access -> sandbox: danger-full-access, approval: never
27
+ #
28
+ # and NO `defaultPreset`. Its schema says `defaultPreset` is "required when composition defaults match
29
+ # no preset".
30
+ #
31
+ # A worker profile patch that hard-codes `policy: ask` therefore breaks the pairing: with
32
+ # `DSH_PERMISSION_MODE=danger-full-access` it composes to (danger-full-access, ask), which matches NO
33
+ # preset, leaves `defaultPreset` unset and unsatisfiable, and can stop the `permission` service from
34
+ # loading -- i.e. the ACP process never comes up and the worker becomes UNREACHABLE. That is the F24
35
+ # failure shape, reached through a configuration combination rather than through code.
36
+ #
37
+ # A hard-coded `ask` was the RIGHT call when it was made (F18's round: it turns a sandbox refusal into
38
+ # a request the master can see, instead of a silent failure). It is retained here for exactly those
39
+ # cases -- the expression below still yields `ask` for every mode except full access. What changes is
40
+ # only that the profile stops DISAGREEING with the base composition about the full-access pairing.
41
+ #
42
+ # The consequence must be stated plainly rather than buried: A FULL-ACCESS WORKER ALSO RUNS WITH
43
+ # APPROVAL POLICY `never`. That is DSH's own preset semantics and it is coherent -- with an
44
+ # unrestricted sandbox there is no refusal left for the approval queue to surface. It is not a bypass
45
+ # of the queue; it is the queue having nothing to ask about. Every other mode keeps `ask`, and so
46
+ # does a deployment that sets no mode at all.
47
+ # ---------------------------------------------------------------------------
48
+
49
+ - id: approval
50
+ name: '@deepseek-ai/dsh-user-approval'
51
+ config:
52
+ # MUST stay this expression, not a literal. It mirrors `dsh-base` byte for byte for the modes that
53
+ # yield `ask`, so this entry is a no-op except when a full-access deployment is in force.
54
+ #
55
+ # THE DOUBLE QUOTES ARE LOAD-BEARING, and leaving them out is not a style choice. The expression
56
+ # contains ": " (before 'ask' in the ternary), and YAML reads an unquoted ": " as a MAPPING rather
57
+ # than as part of a scalar -- so the unquoted form parses as the object {"...? 'never'": "'ask'"},
58
+ # and Cordis rejects it: `$.policy expected "ask" | "never" but got {"[object Object]":"ask"}`.
59
+ # `dsh-base` quotes it for exactly this reason (cordis.patch.yml:248).
60
+ #
61
+ # MEASURED 2026-10-15: with the quotes missing, TWO entries failed to activate (approval failed
62
+ # validation; permission-presets then sat "pending (waiting for service: approval)") while the
63
+ # agent still answered and the sandbox mode still applied -- so nothing looked broken unless
64
+ # stderr was read. A guard that checks the expression's tokens does NOT catch this; only running
65
+ # it, or checking the quoting, does.
66
+ policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"