anywhere-labs/deepseek-harness-desktop 如何围绕上游演进:Submodule、版本溯源与非 Fork 架构

0 阅读17分钟

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 交给操作系统,新窗口请求统一拒绝。与此同时,第三方插件只能消费有限的 desktopProfilesdesktopPnpm 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 最小注入
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 权限:

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 都不会被当作同源。

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 中打开。

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 只有 desktopProfilesdesktopPnpm。前者给出当前 Profile 身份、只读发现和安全切换;后者提供受管包操作。desktopRuntimedesktopPnpmBootstrap、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 或全局命令目录。

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 完全不改。

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 签名/公证
安装前有序退出 CordisWindows Authenticode/SmartScreen
失败不破坏当前版本安装升级、回滚与发布者身份

准确区分“容器完整”和“来源可信”是安全文档最重要的诚实之一。

九、安全测试应该覆盖什么

  1. 两种窗口模式都保持 Node integration 关闭、context isolation 与 sandbox 开启。

  2. 同源导航允许,不同端口、主机、协议、畸形 URL 与 redirect 被阻止。

  3. 新窗口永远不在当前 BrowserWindow 打开,允许协议只交给系统。

  4. Client 插件无法读取 Host service 或 Electron 私有对象。

  5. 包管理参数按 argv 传递,NUL、相对 invokingDir 与并发操作被拒绝。

  6. operation 取消、spawn failure 与 generation teardown 回收完整进程树。

  7. 私有 shim 只出现在应用/终端子环境,系统 PATH 和 shell 配置不变。

  8. Windows ACL adapter 只重写精确 runner,失败绝不降级为不受限执行。

  9. 更新拒绝非法/旧版本、超大或损坏容器,并要求明确用户确认。

  10. 签名 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,第二次决定是否加载真正实现。

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 失败回退,因为真正危险的往往不是缺少一个安全开关,而是边界在便利性压力下被悄悄绕开。