New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

dsh-desktop-bridge

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dsh-desktop-bridge

Third-party, non-official DSH read-only desktop bridge: exposes the current session's stable identity, its bound project directory, a public progress summary, and one localhost-only read-only Fetch route on the existing /api shared channel. Returns unavai

bridge
latest
npmnpm
Version
0.0.1-bridge
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

dsh-desktop-bridge — 只读桌面桥接最小实现

第三方、非官方。不是 DeepSeek 产品,不是官方发布。 兼容 DSH 版本:dsh-v0.1.6-alpha.1,已在以下上游提交核对: 0a15e36e7f82b6ed45af6fa9759f29b40dcd965d。

本包回答且只回答四个只读问题:

目标字段已核准的上游来源
1. 当前会话的稳定标识sessionIdAgent.id / Session.id(SessionId,branded)
2. 绑定到该会话的项目目录projectDirectorySession.header.cwd(绝对路径,可选)
3. 公开的进度摘要progressAgent.status + Session.seq + session/event 事件流
4. 仅本机、只读的受控接口GET /api/desktop/bridgectx.connection.fetch.register()

它不发送消息、不返修、不取消、不远程控制、不写任何设置、 不读任何凭据、不调用任何 provider、不发起任何网络请求。 这不是"尚未实现",而是结构性不可达:见下文"结构性只读"。

一、先核对扩展点:桌面端到底有没有可用的插件/IPC 入口

任务要求先核对当前 DSH 源码是否确实提供可扩展的桌面插件/IPC 入口,没有就停止。 核对结论是有,且已定位。证据链如下,全部来自只读的上游参考树。

1.1 ctx.webServer 在桌面端不存在 —— 这是最容易被误选的入口

apps/desktop-host/config/desktop.cordis.patch.yml 逐行禁用了 Web 组合的多行:

# Electron reuses the browser composition without its network and browser-launch rows.
- id: web-startup
  disabled: true
- id: webserver
  disabled: true
- id: web-runtime
  disabled: true
- id: client-hmr
  disabled: true
- id: open-in-app
  disabled: true
- id: ui-open-in-app
  disabled: true
- id: directory-picker
  disabled: true

apps/desktop/README.md 的 Known limitations 与之互相印证:

The Web "Open In..." action is disabled in Desktop, because its host plugin needs an HTTP route; Desktop does not provide webServer.

同一份 README 还说明该应用 opens no listening port:Electron 外壳启动一个 捆绑的 Node 子进程,用带版本号的分帧字节管道承载 Fetch 流量( DESKTOP_REQUEST_PIPE_FD / DESKTOP_RESPONSE_PIPE_FD),Node IPC 只承载子进程 生命周期控制,渲染进程资源由 dsh-app:// 协议提供。

结论:任何注册到 ctx.webServer 的插件在桌面端永远不会被触达。 packages/host/webserver 是给 Web 组合用的 node:http 命名路由注册表, 它不在桌面组合里。

1.2 ctx.connection 才是桌面端实际使用的载体

同一份 patch 保留了 connection 并只注入了 credentials:

- id: connection
  inject:
    - credentials
  config: {}

apps/desktop-host/src/index.ts 对每一个 dsh-app:// Fetch 显式分派进它:

const connection = ctx.get('connection')
const api = connection.createSharedFetchHandler('/api')
const assets = assetHandler(ctx, absoluteRuntime)
const streams = remoteStreamHandler(ctx)
// ...
const response = url.pathname === DESKTOP_STREAM_PATH
  ? await streams.fetch(request)
  : url.pathname.startsWith('/api/')
    ? await api.fetch(request)
    : await assets.fetch(request)

所以 /api/* 在桌面端是真实可达的。

1.3 fetch.register() 不依赖 ctx.webServer

packages/client/connection/src/rpc-host.ts 里,注册由读取方 Context 的 fiber 持有,与 webserver 无关:

private registerFetchRoute(owner: Context, route: ConnectionFetchRoute): () => Promise<void> {
  assertFetchRoute(route)
  return owner.effect(() => {
    if (this.fetchRoutes.has(route.path)) throw new Error(...)
    this.fetchRoutes.set(route.path, registered)
    return () => { this.fetchRoutes.delete(route.path) }
  }, `client-connection: ${route.path} Fetch route`)
}

createSharedFetchHandler('/api') 先按精确路径查 fetchRoutes,只有未命中时才 交给 RPC 拦截器:

const route = this.fetchRoutes.get(pathname)
if (route?.methods.has(request.method) === true) return route.fetch(request)
const endpoint = endpointFromPath(channel, pathname)
const interceptor = this.interceptors.get(channel)
if (endpoint === undefined || interceptor === undefined || !interceptor.matches(endpoint))
  return Promise.resolve(new Response('not found', { status: 404 }))
return interceptor.fetchHandler.fetch(request)

这就是本包采用的扩展点。

1.4 为什么不用 Typert Remote 方法

packages/api/gateway/src/index.ts 的拦截器只认领自己匹配的端点:

ctx.inject(['connection'], (connectionCtx) => {
  connectionCtx.connection.rpc.intercept(
    '/api',
    endpoint => this.claimsEndpoint(endpoint),
    (endpoint, payload, signal) => this.dispatchRpc(endpoint, payload, signal),
  )
})

docs/api-gateway.md 写明网关 "claims only two-segment endpoints that have a strict descriptor or active SRC marker"。一个远端方法还需要生成的 typert.remote-client.* 产物,而严格流水线只为上游构建树内的 Host 包生成这些产物; 客户端半边还必须由 @deepseek-ai/dsh-api-remotes 组装挂载 —— 那是上游组合修改。 树外插件无法满足。

本包因此走文档化的树外路径:在共享 /api 通道上注册一条精确 Fetch 路由。 它不需要任何生成产物,也不需要任何上游改动。

路由此刻意采用两段式(desktop/bridge),使其在形状上不可能与 <namespace>/<method> 的 Remote 地址混淆;网关对 desktop/bridge 既无严格描述符 也无 SRC 标记,因此会拒绝认领,精确路由先被解析。

1.5 停止条件未被触发

上述四点全部可从当前源码直接证实,因此不需要触发工单里的停止条件 ("如果无法从当前 DSH 源码确认可用的插件/IPC扩展点,停止实现")。核对过程未修改 上游参考目录中的任何文件。

二、数据契约

2.1 成功

{
  "ok": true,
  "data": {
    "version": 1,
    "sessionId": "session-1",
    "projectDirectory": "/abs/path",
    "progress": {
      "status": "idle",
      "turn": 2,
      "step": 1,
      "eventCount": 41,
      "lastEventType": "step/end"
    },
    "source": "agents+session-header+projection",
    "readOnly": "read-only"
  }
}

2.2 不可用(未配置 / 桌面端不支持)

{
  "ok": false,
  "status": "unavailable",
  "reason": "no-live-session",
  "detail": "the session store holds no live session"
}

每一个字段永远存在。 projectDirectory、status、turn、step、 lastEventType 上的 null 表示宿主没有报告该事实,绝不是被替换的默认值。 拒绝体只有上面四个键,一个会话都藏不进去。

2.3 冻结键集

常量键
SNAPSHOT_FIELDSversion sessionId projectDirectory progress source readOnly
PROGRESS_FIELDSstatus turn step eventCount lastEventType
UNAVAILABLE_FIELDSversion status reason detail
OK_ENVELOPE_FIELDSok data
REFUSAL_ENVELOPE_FIELDSok status reason detail

运行时逐字段组装每个投影,然后断言自身键集等于冻结列表 (assertFieldSet)。所以"多出一个字段"不是靠读代码相信,而是每次发布都被证明。

2.4 已发布的 unavailable 原因

键值含义
NO_SESSION_STOREno-session-store宿主完全没有可读的会话存储
NO_LIVE_SESSIONno-live-session存储可读但为空
AMBIGUOUS_SESSIONambiguous-session存在多个活跃会话,选任何一个都是猜测
SESSION_NOT_FOUNDsession-not-found指定的会话在此进程中不活跃
SESSION_READ_FAILEDsession-read-failed读取失败
NO_API_CARRIERno-api-carrier共享 /api 载体缺失,路由无法挂载
DISPOSEDdisposed桥接已释放,不再持有任何宿主绑定

未配置的桌面启动(活跃 Host + 空存储)正是 no-live-session: 它被报告为 unavailable,绝不会变成一个伪造的 session id。

三、会话选择策略

上游没有"当前会话"这个接缝:ctx.agents 与 ctx.sessions 只暴露 get/list/roots,没有 active 指针。所以选择是显式的,永不猜测:

  • 指定了 id → 精确匹配;未命中即 session-not-found。
  • 存储中恰好一个会话 → 它就是当前会话。
  • 多个会话 → 仅当 roots()(只要顶层 agent)恰好得到一个时才用它消歧, 这样子 agent 的存在不可能导致错误选择。
  • 其余一切情况 → ambiguous-session 拒绝。

四、结构性只读

本包在整个运行期只会调用五个宿主成员,且全部是读取:

list、roots(选择)、get(已解析会话的状态)、on(观察)、 fetch.register(挂载)。

没有写路径,没有消息发送,没有返修,没有取消,没有 steering,没有远程控制, 没有设置变更 —— 因此那些能力不是"未实现",而是不可达。

scripts/build.mjs 会对可执行源码(注释已剥离)静态扫描禁止能力词表, 然后真的加载并运行本模块,用内联 mock 宿主验证行为。

4.1 敏感字段过滤

结构过滤(逐字段组装、从不展开宿主对象)阻止意外字段被发布。 值过滤(src/redact.js)阻止意外取值被发布,这是更难的一半:

SECRET_SHAPES 拒绝:Bearer <token>、PEM 私钥块、三段式 JWT、 已知 provider 前缀(sk- rk- pk- ghp- glpat- gho- ghs-)、 以及 secret 命名字段的赋值形态。

此外:

  • 标识符:40+ 字符的连续 base64url 串被拒绝(宁可过度拒绝);
  • 路径:必须是绝对路径(POSIX / 盘符 / UNC 三种拼写);
  • 状态:只发布闭集 idle / running,其余一律 null;
  • 计数:必须是非负安全整数。

不读取清单:不读凭据记录、不读设置值、不读 header、不读 cookie、 不读完整提示词、不读响应正文、不读工具参数、不读工具结果。

4.2 为什么不读会话历史

上游 Agent Note 2026-09-09-deprecate-synchronous-session-event-reads 将 Session.eventAt / snapshotEvents / ownEvents 对新调用方标记为已废弃。 因此本包完全不碰历史。

它改为观察 session/event 事件流 —— 监听器收到完整事件对象,但只读一个属性 type,在证明它是一个安全短标签(isSafeLabel:无控制字符、≤128 字符、无秘密形态) 之后才保留。事件的 data(消息、工具参数、结果、提示词)从不被读取。

turn / step 因此只在组合注入投影解析器时发布;否则明确为 null。 从消息载荷里推断它们,正是本契约禁止的那种猜测。

五、路由行为

请求响应
GET /api/desktop/bridge200,content-type: application/json; charset=utf-8,成功信封或 unavailable 信封
任何其他方法405,allow: GET

200 是刻意的:一个健康但恰好没有会话的宿主不是错误状态,回 5xx 只会让调用方 反复重试一个不会改变的状态。非 GET 是唯一的真实客户端错误。

若宿主没有 /api 载体:apply() 仍然成功,宿主侧读取仍然可用, route.mounted === false 且带一个不含取值的 reason。 因此安装本插件永远不会让 Host 启动失败。

5.1 信任边界

本包不安装自己的信任策略。桌面端上载体是一条私有分帧字节管道,只有 Electron 外壳的 dsh-app:// 处理器能到达;Web 端上 /api 前缀承载由 dsh-client-connection 持有的 Host/Origin 围栏与浏览器会话检查。在此注册的路由继承其载体已有的策略。

请求因此完全无法影响答案:本包唯一读取的请求属性是它的方法 (因为路由回答的是一个关于宿主的固定问题)。URL、查询串、body、header、cookie 一概不读 —— 单测用一个 method 安全、而 url/body/headers/cookies 取值即抛错的敌意请求对象证明了这一点。

六、用法

import { apply } from 'dsh-desktop-bridge'

// Cordis 插件入口:创建桥接、挂载只读路由、向宿主注册幂等释放。
export const name = 'desktop-bridge'
export const inject = ['connection']   // 可选:缺失时路由报 unavailable,不抛错
export const apply = (ctx) => apply(ctx)

读取:

import { createBridge } from 'dsh-desktop-bridge'

const bridge = createBridge(ctx, { sessionId: 'session-1' })  // 省略则自动发现
bridge.read()          // { ok: true, data } | { ok: false, status, reason, detail }
bridge.snapshot()      // 投影本身,或 available:false 记录
bridge.route()         // 挂载只读路由(幂等)
bridge.stats()         // 仅计数,不含标识/路径/状态值
bridge.dispose()

6.1 选项

选项默认含义
sessionId自动发现读取指定的一个活跃会话
observeEventstrue观察 session/event 的最后一个事件类型;只保留 type
progress无注入的投影解析器,提供 turn / step

七、本地检查

node --test packages/desktop-bridge/test/*.test.mjs   # 针对性单元测试
node packages/desktop-bridge/scripts/build.mjs        # 零依赖构建闸

或在仓库根:

pnpm test
pnpm run build

scripts/build.mjs 是确定性的、零依赖的,且不需要网络:

  • 清单表面(main/types/exports/files 指向真实存在的文件);
  • 随包文件存在;
  • README 实质性(长度、必需说明 token,含本 README 里的上游提交哈希);
  • 声明与运行时导出集双向一致;
  • 冻结字段元组与 .d.ts 声明一致;
  • 禁止能力源码扫描(先剥离注释,因为本包的注释合法地提到被禁止的词以说明 永不使用它——扫描原文只会命中免责声明而不是代码);
  • 真实加载模块并用内联 mock 宿主跑通成功路径、无会话路径与过滤路径。

八、未完成项与风险

未完成项

  • turn / step 需要组合注入的投影解析器才会非 null;本包不自带该投影, 因为上游只通过 TurnBoundaryProjection 暴露它。
  • 未在真实 Electron 桌面上端到端运行。验证范围是针对性单元测试加内联 mock 宿主, 加上对上游接缝的源码核对。

风险

  • 选择策略依赖 list() / roots() 的语义。若未来上游为 roots() 换上不同含义, 消歧步骤会失效;届时第 3 步应降级为 ambiguous-session 而不是猜测。
  • Session.seq 是日志长度,不是事件条数。它在语义上是"下一个序号"。 本包把它作为 eventCount 发布;消费方若需要严格的事件计数,应视为近似。
  • session/event 观察器是进程内最后一次观察,不是历史的权威视图。 桥接在读取之前挂载它,因此它只会看到挂载之后的事件。
  • 值过滤是启发式的。SECRET_SHAPES 覆盖的是已知形态;一个不含已知形态的 全新凭据形状不会被拦下。缓解手段是结构过滤:本包发布的字段集里根本没有可以 承载凭据的字段。
  • isPublishableIdentifier 会对 40+ 字符的连续串过度拒绝。这是刻意的取舍: 桥接拒绝回答是正确的,桥接回显一个 token 不是。

FAQs

Package last updated on 20 Sep 2026

Related posts