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

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 最小注入
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 只有 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 或全局命令目录。

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
失败不破坏当前版本 安装升级、回滚与发布者身份

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

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

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

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

相关推荐
用户3126874877201 小时前
一条 Prompt 就能劫持你的 Agent!OWASP Agentic Top 10 与零信任防御实战
agent
麻瓜pro1 小时前
DeepSeek Harness远程访问
agent
三雒2 小时前
拆解桌面Agent:WorkBuddy 删除保护机制
agent·ai编程
鱼饼Y2 小时前
DeepSeek Harness 来了!从用户界面分析DSH
agent·deepseek
RebornL2 小时前
DeepSeek Harness (DSH) 插件机制解析:一切皆插件的 Agent 运行时
agent
用户9983834541132 小时前
给 Agent 加一个「挑刺的审稿人」——Critic 与自纠错回环
agent
用户9983834541132 小时前
用LLM + Neo4j 给生物医学文献建知识图谱
agent
用户9983834541132 小时前
从研究到可交付产物——报告生成、导出与容器化部署
agent
Lear2 小时前
MinerU:把文档变成大模型能"读"的样子
agent