anywhere-labs/deepseek-harness-desktop 如何围绕上游演进:Submodule、版本溯源与非 Fork 架构
摘要
作为长期参与桌面端与本地开发工具安全设计的工程师,我经常看到一种危险误解:只要 Electron 页面加载的是 127.0.0.1,应用就天然安全。实际上,本地 Web 服务仍可能接收恶意导航、被不受信任内容利用;renderer 一旦获得 Node.js 或宽泛 IPC,就可能越过浏览器沙箱访问用户文件;插件安装、终端和 PowerShell sandbox 还会把风险扩展到完整子进程树。本文专门分析社区项目 anywhere-labs/deepseek-harness-desktop 的安全架构,而不是为官方 DeepSeek Harness 做笼统背书。DSH Desktop 的核心策略,是让 Electron main 进程承担原生权限与 Host Cordis,renderer 继续作为受限 Web 客户端:contextIsolation、Chromium sandbox 与 webSecurity 默认开启,Node integration 关闭,不建立面向页面的 Electron preload bridge;窗口导航被锁定到启动时确定的 loopback origin,外部 HTTP/HTTPS/mailto 交给操作系统,新窗口请求统一拒绝。与此同时,第三方插件只能消费有限的 desktopProfiles 与 desktopPnpm contract,不能获取 BrowserWindow、托盘、安装器或私有 Node helper;包管理与终端使用应用私有 shim 和受管环境,不修改系统 PATH;Windows ACL PowerShell 路径只做精确 argv 适配,confinement 失败也不会降级为不受限执行。本文会以威胁面、信任区、导航策略、插件权限、子进程、更新和发布者身份为主线,结合源码片段与流程图解释这些选择能防什么、不能证明什么。我的目标不是把一个开源项目描述成"绝对安全",而是展示一套边界清楚、默认拒绝、可以测试且会明确暴露剩余风险的桌面安全设计。
项目身份说明:本文专门讨论
anywhere-labs/deepseek-harness-desktop。它是基于deepseek-ai/deepseek-harness构建的社区桌面项目,并非 DeepSeek 官方产品。

图1 DSH Desktop 的 Web 界面运行在受限 renderer 中,原生权限留在 Electron main
一、先建立威胁模型
DSH Desktop 不是纯展示应用。它承载 Agent、工具执行、Profile、插件安装、系统终端、更新下载与 Windows sandbox,因此需要同时保护本地数据、执行权限、Profile 完整性、安装包身份和运行时凭据。
| 资产 | 主要威胁 | 首要控制点 |
|---|---|---|
| DSH 会话、设置和凭据 | renderer 越权读取、错误 Profile 被修改 | Host service 边界、sandbox |
| 用户工作区 | 恶意导航、插件或 shell 逃逸 | origin 限制、工具权限、ACL sandbox |
| Profile manifest/lockfile | 并发 pnpm、参数注入、路径混淆 | argv、单操作门、权威 current |
| Electron 原生能力 | preload/IPC 暴露过宽 | 不向 renderer 发布 Electron bridge |
| 更新安装包 | 伪造版本、损坏容器、冒充发布者 | SemVer、容器 gate、签名/公证 |
| 签名与公证凭据 | 构建脚本或测试意外读取 | release step 最小注入 |
text
flowchart LR
subgraph Privileged["高权限区:Electron Main"]
E["Electron Runtime"]
H["Cordis Host"]
S["Subprocess / Sandbox"]
end
subgraph Local["本机传输边界"]
W["127.0.0.1<br/>HTTP + WebSocket"]
end
subgraph Restricted["低权限区:Renderer"]
R["Web UI / Client Plugins"]
end
subgraph External["外部世界"]
X["网页 / mailto / 下载服务"]
end
H <--> W
W <--> R
H --> S
R -. "无 Node / 无原生 IPC" .-> E
R -->|"外链交给 OS"| X
classDef privileged fill:#b91c1c,color:#fff,stroke:#7f1d1d,stroke-width:2px;
classDef transport fill:#f59e0b,color:#111827,stroke:#d97706,stroke-width:2px;
classDef restricted fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef external fill:#64748b,color:#fff,stroke:#334155,stroke-width:2px;
class E,H,S privileged;
class W transport;
class R restricted;
class X external;
图2 信任区划分:本地不等于同一权限,renderer 与 main 仍需隔离
威胁模型最重要的结论是:页面内容、第三方 Client 插件和包管理器输出都不能因为"来自本机"就自动可信。
二、Renderer 默认没有 Node 权限
兼容与高级两种窗口都复用同一组安全配置。模式只改变原生材质和布局,不改变 renderer 权限:
ts
const options: BrowserWindowConstructorOptions = {
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
},
}
nodeIntegration: false 阻止页面直接 require('node:fs');contextIsolation: true 把页面 JavaScript 与 Electron 隔离上下文分开;sandbox: true 启用 Chromium renderer sandbox;webSecurity: true 保留同源和相关浏览器安全策略。更关键的是,项目没有添加一个"功能齐全"的 preload bridge 把这些权限重新包装回页面。
| 配置 | 防护价值 | 不能单独解决的问题 |
|---|---|---|
nodeIntegration: false |
页面不能直接使用 Node 内置模块 | preload 若过宽仍可越权 |
contextIsolation: true |
隔离页面与特权上下文 | 暴露对象仍需最小设计 |
sandbox: true |
限制被攻陷 renderer 的系统能力 | main 进程逻辑仍需校验 |
webSecurity: true |
保留同源/CORS 等 Web 约束 | 本地 Host 自身仍需安全路由 |
这种配置与 Electron 安全清单 的方向一致,但配置项不是安全终点。窗口还必须控制能加载和跳转到哪里。
三、把窗口锁在精确 Loopback Origin
Host 使用 127.0.0.1 和临时端口提供 Web surface。创建窗口时,runtime 从页面 URL 计算唯一 origin;frame navigation 与 redirect 事件只要离开该 origin就被阻止。比较的是完整 origin,而不是字符串前缀,因此不同端口、不同主机名或伪造相似 URL 都不会被当作同源。
ts
const origin = new URL(spec.url).origin
const navigate = (event: Electron.Event<{ url: string }>): void => {
let targetOrigin: string | undefined
try {
targetOrigin = new URL(event.url).origin
} catch {
targetOrigin = undefined
}
if (targetOrigin !== origin) event.preventDefault()
}
window.webContents.on('will-frame-navigate', navigate)
window.webContents.on('will-redirect', navigate)
setWindowOpenHandler 对任何新窗口都返回 deny。如果目标协议是 HTTP、HTTPS 或 mailto,应用调用系统 shell.openExternal();无法解析的 URL 和其他 scheme 直接拒绝。这样,外部网页不会在拥有本地 DSH origin 上下文的 BrowserWindow 中打开。
text
flowchart TD
A["Renderer 发起导航/新窗口"] --> B{"当前窗口导航?"}
B -- "是" --> C{"origin 完全相同?"}
C -- "是" --> D["允许"]
C -- "否" --> E["preventDefault"]
B -- "新窗口" --> F{"http/https/mailto?"}
F -- "是" --> G["交给系统外部应用"]
F -- "否" --> H["拒绝"]
G --> H
classDef event fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef decision fill:#f59e0b,color:#111827,stroke:#d97706,stroke-width:2px;
classDef allow fill:#10b981,color:#fff,stroke:#047857,stroke-width:2px;
classDef deny fill:#ef4444,color:#fff,stroke:#b91c1c,stroke-width:2px;
class A event;
class B,C,F decision;
class D,G allow;
class E,H deny;
图3 导航策略:同源页面留在窗口,外部资源脱离特权载体
四、为什么不增加 Electron IPC 插件系统
DSH 已经拥有 Host route、RPC、client metadata、service 和 slot。DSH Desktop 选择复用 loopback carrier,而不是再建立 renderer-to-main IPC 插件注册表。这减少了三类风险:无需维护另一套鉴权与序列化协议;第三方 Client 插件不会因为运行在 Electron 中自动获得原生能力;兼容模式可以保持官方 Web UI 的组合语义。
第三方有界面需求时,应让 Host 插件通过普通 route/RPC 提供经过校验的领域操作,renderer 只接收任务状态与结果。不要传 PID、绝对可执行文件路径、BrowserWindow handle 或任意文件读写函数。页面到 Host 的每个操作仍要做身份、参数、路径和业务权限校验,loopback 不是免鉴权标志。
安全接口应表达"安装某个经过校验的插件"或"选择某个已发现的 Profile",而不是表达"执行任意命令"或"调用任意 Electron 方法"。
五、插件权限遵循最小 Contract
对第三方公开的 Desktop service 只有 desktopProfiles 与 desktopPnpm。前者给出当前 Profile 身份、只读发现和安全切换;后者提供受管包操作。desktopRuntime、desktopPnpmBootstrap、BrowserWindow、托盘注册表、更新 adapter、Node helper 与 ELECTRON_RUN_AS_NODE 都是内部能力。
| 能力 | 第三方状态 | 安全理由 |
|---|---|---|
| 当前 Profile name/dir | 公开、只读 snapshot | 防止从 argv/URL 猜错目标 |
| Profile 发现与选择 | 公开、受控重启 | 防止原地替换活跃运行时 |
| 插件 add/remove/update | 公开、单 operation | 防止并发破坏 lockfile/Profile |
| BrowserWindow/Tray | 不公开 | 避免 UI 插件获得原生控制权 |
| 私有 Node/ABI 环境 | 不公开 | 防止绕过受管 subprocess |
| 安装器与更新路径 | 不公开 | 维持发布信任边界 |
desktopPnpm 使用 argv 而不是 shell 拼接,拒绝空参数和 NUL;runPlugin() 还要求绝对调用目录。每个 generation 同时只允许一个包操作,取消和 teardown 作用于完整进程树,直到后代退出前都不会释放 operation gate。
六、私有命令环境不污染系统
安装包需要 pnpm、DSH CLI 和 Electron-backed Node,但普通用户不应为此修改系统 PATH。Launcher 在应用私有 user-data 目录创建命令 shim,只对 Electron main 的兼容环境或 Desktop 打开的终端进程前置路径;不会写入系统环境变量、shell profile、Profile .env 或全局命令目录。
text
flowchart LR
APP["DSH Desktop"] --> DIR["user-data 私有命令目录"]
DIR --> DSH["dsh shim"]
DIR --> PNPM["pnpm shim"]
DIR --> NODE["Electron-backed node shim"]
DIR --> TERM["Desktop 打开的终端环境"]
SYS["系统 PATH / shell 配置"] -. "保持不变" .-> DIR
classDef app fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef private fill:#8b5cf6,color:#fff,stroke:#6d28d9,stroke-width:2px;
classDef system fill:#64748b,color:#fff,stroke:#334155,stroke-width:2px;
class APP app;
class DIR,DSH,PNPM,NODE,TERM private;
class SYS system;
图4 私有命令环境:应用和其子进程可用,用户全局环境不被持久修改
终端欢迎页可以显示当前 Profile 与 DSH home,但打开后 Profile 身份被固定,之后托盘切换不会悄悄改变已打开终端的目标。显式 --profile 仍优先,避免"界面看似切换、旧终端实际写到新位置"的不透明行为。
七、Windows ACL Sandbox 不做静默降级
Windows 上游 PowerShell sandbox 使用 ACL runner。Electron 可执行文件需要通过 ELECTRON_RUN_AS_NODE=1 运行 JavaScript entry,但这个变量不能泄漏进真正受限的 PowerShell 子进程。Desktop adapter 只在平台是 Windows、宿主确为 Electron、program 等于当前 executable、runner 等于解析出的上游 runner 时,才插入自己的 trampoline;其他 argv 完全不改。
ts
if (platform !== 'win32'
|| !electron
|| program !== execPath
|| runner !== upstreamRunner) {
return { spec, argv } // 非精确目标不做泛化重写
}
return {
spec: { ...spec, env: { ...spec.env, ELECTRON_RUN_AS_NODE: '1' } },
argv: [execPath, trampoline, upstreamRunner, ...args],
}
trampoline 启动后先清除 ELECTRON_RUN_AS_NODE,再次解析并比对预期 runner,才 import 上游实现。关键点是 Desktop 没有重新实现 confinement policy,只修复 Electron 承载方式;Windows confinement 失败时也不会自动回退到 danger-full-access。安全失败必须显式失败,否则"为了可用性"会变成无提示提权。
八、更新链路的信任分层
更新检查只接受规范 stable Semantic Version,并要求服务端版本严格更高。后台失败保持静默,手动检查会反馈结果;只有用户确认后才访问下载入口。下载文件被限制在 1 GiB 以内,写入私有版本目录,并在交付前检查 DMG 或 PE 容器是否完整。
这些控制能防止无效响应、旧版本、异常大文件和明显损坏容器,却不能证明发布者身份。macOS 需要签名和 notarization;Windows 本地 dist:win 产物按设计未签名,正式发行仍需要 Authenticode、publisher 验证、SmartScreen 与真实升级测试。
| 已验证 | 尚需独立 Release Gate |
|---|---|
| SemVer 规范且版本更高 | 服务端与发布流程权限治理 |
| 用户明确同意下载 | 下载端到端来源证明 |
| 文件大小与 DMG/PE 容器 | macOS 签名/公证 |
| 安装前有序退出 Cordis | Windows Authenticode/SmartScreen |
| 失败不破坏当前版本 | 安装升级、回滚与发布者身份 |
准确区分"容器完整"和"来源可信"是安全文档最重要的诚实之一。
九、安全测试应该覆盖什么
-
两种窗口模式都保持 Node integration 关闭、context isolation 与 sandbox 开启。
-
同源导航允许,不同端口、主机、协议、畸形 URL 与 redirect 被阻止。
-
新窗口永远不在当前 BrowserWindow 打开,允许协议只交给系统。
-
Client 插件无法读取 Host service 或 Electron 私有对象。
-
包管理参数按 argv 传递,NUL、相对 invokingDir 与并发操作被拒绝。
-
operation 取消、spawn failure 与 generation teardown 回收完整进程树。
-
私有 shim 只出现在应用/终端子环境,系统 PATH 和 shell 配置不变。
-
Windows ACL adapter 只重写精确 runner,失败绝不降级为不受限执行。
-
更新拒绝非法/旧版本、超大或损坏容器,并要求明确用户确认。
-
签名 secret 不进入普通 build、test、Loader smoke 和日志。
十、用四个攻击场景检验边界
场景一:第三方 Client 插件尝试读取本地文件。 攻击代码运行在 renderer 中,首先会遇到 nodeIntegration: false,无法直接导入 node:fs;项目又没有提供通用 preload 文件读取接口,因此它必须转向普通 Host route。此时权限判断回到插件自己的 Host API:如果该 API 只接受经过规范化、授权的领域参数,攻击链会在服务边界终止;如果插件自己发布了"读取任意绝对路径"接口,Desktop 的 renderer sandbox 也无法替它修复业务越权。这个场景说明平台安全与插件 API 安全缺一不可。
场景二:页面中的外部链接试图把窗口带到钓鱼站。 当前 frame navigation 和 redirect 会比较完整 origin,目标站点无法留在原 BrowserWindow;window.open() 也被统一 deny。允许的 HTTP/HTTPS/mailto 只会交给系统默认应用。即使目标网页恶意,它获得的是普通外部浏览器上下文,而不是 DSH Desktop renderer 的本地页面状态。测试时不能只覆盖直接点击,还应覆盖 30x redirect、脚本导航、不同 loopback 端口、IPv6 localhost 和畸形 URL。
场景三:恶意包名尝试注入 shell 参数。 desktopPnpm.runPlugin() 接收 argv 数组,目标字符串不会与命令拼接为一段 shell 文本;NUL 会被拒绝,调用目录必须为绝对路径。尽管如此,调用插件仍要按自己的 registry、scope 或本地 source 信任策略校验 target,因为"不能注入 shell"不等于"允许安装任何代码"。包一旦被用户授权安装,下一代 Host 中就可能获得该插件声明的正常能力,供应链审查属于更高一层控制。
场景四:攻击者诱导 Windows sandbox 启动错误 runner。 Desktop adapter 会同时检查平台、Electron 身份、当前 executable 和上游 runner 的精确路径;trampoline 内再次解析并比对 runner,路径不一致就以失败码退出。它没有接受"长得像 runner 的任意脚本",也不会在失败后重新执行无 sandbox PowerShell。这里采用了两次身份确认:第一次决定是否适配 argv,第二次决定是否加载真正实现。
text
flowchart TD
A["潜在攻击输入"] --> B{"进入哪条边界?"}
B --> C["Renderer 文件访问"]
B --> D["外部导航"]
B --> E["插件安装参数"]
B --> F["Windows ACL Runner"]
C --> C1["无 Node / 无通用 preload"]
D --> D1["精确 origin + deny new window"]
E --> E1["argv + 校验 + 单操作门"]
F --> F1["双重 runner 身份检查"]
C1 --> G["仍需领域 API 授权"]
D1 --> H["交给外部浏览器"]
E1 --> I["仍需供应链信任"]
F1 --> J["失败不降级"]
classDef threat fill:#b91c1c,color:#fff,stroke:#7f1d1d,stroke-width:2px;
classDef decision fill:#f59e0b,color:#111827,stroke:#d97706,stroke-width:2px;
classDef control fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef residual fill:#64748b,color:#fff,stroke:#334155,stroke-width:2px;
class A threat;
class B decision;
class C,D,E,F,C1,D1,E1,F1 control;
class G,H,I,J residual;
图5 红队场景推演:技术控制阻断路径,同时保留剩余责任
十一、当前控制不能替代什么
首先,renderer sandbox 不能替代第三方插件审计。Host 插件本来就在高权限进程中运行,用户安装来源、package 签名、维护者信誉、依赖供应链和插件自身的权限设计仍然重要。Desktop 的有限 service contract 减少了原生攻击面,却不会把任意第三方代码变成无害代码。
其次,loopback origin 限制不能替代 Host route 的认证、CSRF/跨站请求判断和输入验证。窗口只访问同源页面不代表其他本地进程永远无法连接端口;每个高风险 route 仍要按 DSH 的真实 carrier 与身份模型设计,不能把"监听 127.0.0.1"当作唯一授权机制。
再次,容器检查不能替代密码学身份。合法 PE/DMG 只说明文件结构可被平台识别,不能说明它由预期发布者生成。正式分发需要 HTTPS 基础设施、签名、公证、证书保护、版本发布权限、可撤销机制和目标机器验证共同成立。
最后,代码级策略不能替代运维响应。依赖漏洞、证书泄露、版本服务被篡改或第三方插件下架都需要监控、公告、撤回和升级流程。安全架构的意义是让影响范围与责任位置清晰,使响应者知道应该撤销哪种凭据、阻断哪个入口、重建哪类产物,而不是承诺永远不会发生安全事件。
参考资料
总结
完整梳理 anywhere-labs/deepseek-harness-desktop 的窗口、导航、插件 service、终端环境、Windows ACL 与更新链路后,我认为它最值得借鉴的安全思想,是没有把"本地应用"当作可以取消边界的理由。renderer 即使只加载 127.0.0.1,仍被关闭 Node integration、启用 context isolation 与 Chromium sandbox;窗口即使服务于同一个产品,仍只能停留在启动时确定的精确 origin,外部链接必须脱离 BrowserWindow;第三方插件即使运行在 Host Cordis,也只能消费表达领域意图的 Profile 与受管 pnpm contract,不能触摸原生窗口和私有启动事实。子进程层面,包操作采用 argv、单 operation gate、可取消 handle 与完整进程树回收,私有 shim 只影响应用创建的环境,不在用户系统留下全局配置;Windows ACL 适配只处理完全匹配的上游 runner,并把失败保留为失败,而不是用不受限执行换取表面可用。更新模块也明确承认版本和容器校验不能替代发布者身份,把签名、公证、Authenticode 与升级测试放在独立 gate。对我而言,这种设计的价值不在于声称系统不存在漏洞,而在于每一条高权限路径都能回答谁发起、传入什么、允许到哪里、何时释放,以及失败后是否会降低保护级别。未来评审任何 Electron AI 工具时,我都会沿用这组问题,并特别警惕宽泛 preload、字符串 shell、全局 PATH 修改、模糊 origin 比较和 sandbox 失败回退,因为真正危险的往往不是缺少一个安全开关,而是边界在便利性压力下被悄悄绕开。