
Security News
Rust 1.100 Will Add Dependency Cooldowns to Cargo
Rust 1.100 will add opt-in dependency cooldowns to Cargo, giving security scanners and maintainers time to catch malicious crate releases.
dsh-desktop-bridge
Advanced tools
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
第三方、非官方。不是 DeepSeek 产品,不是官方发布。
兼容 DSH 版本:dsh-v0.1.6-alpha.1,已在以下上游提交核对:
0a15e36e7f82b6ed45af6fa9759f29b40dcd965d。
本包回答且只回答四个只读问题:
| 目标 | 字段 | 已核准的上游来源 |
|---|---|---|
| 1. 当前会话的稳定标识 | sessionId | Agent.id / Session.id(SessionId,branded) |
| 2. 绑定到该会话的项目目录 | projectDirectory | Session.header.cwd(绝对路径,可选) |
| 3. 公开的进度摘要 | progress | Agent.status + Session.seq + session/event 事件流 |
| 4. 仅本机、只读的受控接口 | GET /api/desktop/bridge | ctx.connection.fetch.register() |
它不发送消息、不返修、不取消、不远程控制、不写任何设置、 不读任何凭据、不调用任何 provider、不发起任何网络请求。 这不是"尚未实现",而是结构性不可达:见下文"结构性只读"。
任务要求先核对当前 DSH 源码是否确实提供可扩展的桌面插件/IPC 入口,没有就停止。 核对结论是有,且已定位。证据链如下,全部来自只读的上游参考树。
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 命名路由注册表,
它不在桌面组合里。
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/* 在桌面端是真实可达的。
fetch.register() 不依赖 ctx.webServerpackages/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)
这就是本包采用的扩展点。
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 标记,因此会拒绝认领,精确路由先被解析。
上述四点全部可从当前源码直接证实,因此不需要触发工单里的停止条件 ("如果无法从当前 DSH 源码确认可用的插件/IPC扩展点,停止实现")。核对过程未修改 上游参考目录中的任何文件。
{
"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"
}
}
{
"ok": false,
"status": "unavailable",
"reason": "no-live-session",
"detail": "the session store holds no live session"
}
每一个字段永远存在。 projectDirectory、status、turn、step、
lastEventType 上的 null 表示宿主没有报告该事实,绝不是被替换的默认值。
拒绝体只有上面四个键,一个会话都藏不进去。
| 常量 | 键 |
|---|---|
SNAPSHOT_FIELDS | version sessionId projectDirectory progress source readOnly |
PROGRESS_FIELDS | status turn step eventCount lastEventType |
UNAVAILABLE_FIELDS | version status reason detail |
OK_ENVELOPE_FIELDS | ok data |
REFUSAL_ENVELOPE_FIELDS | ok status reason detail |
运行时逐字段组装每个投影,然后断言自身键集等于冻结列表
(assertFieldSet)。所以"多出一个字段"不是靠读代码相信,而是每次发布都被证明。
unavailable 原因| 键 | 值 | 含义 |
|---|---|---|
NO_SESSION_STORE | no-session-store | 宿主完全没有可读的会话存储 |
NO_LIVE_SESSION | no-live-session | 存储可读但为空 |
AMBIGUOUS_SESSION | ambiguous-session | 存在多个活跃会话,选任何一个都是猜测 |
SESSION_NOT_FOUND | session-not-found | 指定的会话在此进程中不活跃 |
SESSION_READ_FAILED | session-read-failed | 读取失败 |
NO_API_CARRIER | no-api-carrier | 共享 /api 载体缺失,路由无法挂载 |
DISPOSED | disposed | 桥接已释放,不再持有任何宿主绑定 |
未配置的桌面启动(活跃 Host + 空存储)正是 no-live-session:
它被报告为 unavailable,绝不会变成一个伪造的 session id。
上游没有"当前会话"这个接缝:ctx.agents 与 ctx.sessions 只暴露
get/list/roots,没有 active 指针。所以选择是显式的,永不猜测:
session-not-found。roots()(只要顶层 agent)恰好得到一个时才用它消歧,
这样子 agent 的存在不可能导致错误选择。ambiguous-session 拒绝。本包在整个运行期只会调用五个宿主成员,且全部是读取:
list、roots(选择)、get(已解析会话的状态)、on(观察)、
fetch.register(挂载)。
没有写路径,没有消息发送,没有返修,没有取消,没有 steering,没有远程控制, 没有设置变更 —— 因此那些能力不是"未实现",而是不可达。
scripts/build.mjs 会对可执行源码(注释已剥离)静态扫描禁止能力词表,
然后真的加载并运行本模块,用内联 mock 宿主验证行为。
结构过滤(逐字段组装、从不展开宿主对象)阻止意外字段被发布。
值过滤(src/redact.js)阻止意外取值被发布,这是更难的一半:
SECRET_SHAPES 拒绝:Bearer <token>、PEM 私钥块、三段式 JWT、
已知 provider 前缀(sk- rk- pk- ghp- glpat- gho- ghs-)、
以及 secret 命名字段的赋值形态。
此外:
idle / running,其余一律 null;不读取清单:不读凭据记录、不读设置值、不读 header、不读 cookie、 不读完整提示词、不读响应正文、不读工具参数、不读工具结果。
上游 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/bridge | 200,content-type: application/json; charset=utf-8,成功信封或 unavailable 信封 |
| 任何其他方法 | 405,allow: GET |
200 是刻意的:一个健康但恰好没有会话的宿主不是错误状态,回 5xx 只会让调用方
反复重试一个不会改变的状态。非 GET 是唯一的真实客户端错误。
若宿主没有 /api 载体:apply() 仍然成功,宿主侧读取仍然可用,
route.mounted === false 且带一个不含取值的 reason。
因此安装本插件永远不会让 Host 启动失败。
本包不安装自己的信任策略。桌面端上载体是一条私有分帧字节管道,只有 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()
| 选项 | 默认 | 含义 |
|---|---|---|
sessionId | 自动发现 | 读取指定的一个活跃会话 |
observeEvents | true | 观察 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 指向真实存在的文件);.d.ts 声明一致;未完成项
turn / step 需要组合注入的投影解析器才会非 null;本包不自带该投影,
因为上游只通过 TurnBoundaryProjection 暴露它。风险
list() / roots() 的语义。若未来上游为 roots() 换上不同含义,
消歧步骤会失效;届时第 3 步应降级为 ambiguous-session 而不是猜测。Session.seq 是日志长度,不是事件条数。它在语义上是"下一个序号"。
本包把它作为 eventCount 发布;消费方若需要严格的事件计数,应视为近似。session/event 观察器是进程内最后一次观察,不是历史的权威视图。
桥接在读取之前挂载它,因此它只会看到挂载之后的事件。SECRET_SHAPES 覆盖的是已知形态;一个不含已知形态的
全新凭据形状不会被拦下。缓解手段是结构过滤:本包发布的字段集里根本没有可以
承载凭据的字段。isPublishableIdentifier 会对 40+ 字符的连续串过度拒绝。这是刻意的取舍:
桥接拒绝回答是正确的,桥接回显一个 token 不是。FAQs
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
The npm package dsh-desktop-bridge receives a total of 0 weekly downloads. As such, dsh-desktop-bridge popularity was classified as not popular.
We found that dsh-desktop-bridge demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Rust 1.100 will add opt-in dependency cooldowns to Cargo, giving security scanners and maintainers time to catch malicious crate releases.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.