dsh-plugin-agent-bridge
English | 中文
A two-way agent-to-agent bridge between two DeepSeek Harness hosts over a private network. An agent on one machine can ask the agent on the other machine a question, receive a streamed answer produced with that machine's own workspace and tools, and hold a multi-turn dialogue — all over one authenticated TCP link.
Not affiliated with DeepSeek. This is a community plugin that runs inside a DSH profile.
English
Features
- Two-way link. Either side can open a turn; both directions share one socket.
- Operator page in the GUI. A
Bridge entry in the sidebar (below Automation tasks) opens a full-width chat page: connection state, transcript, Markdown, code blocks with copy, and a composer that sends to the peer agent.
- Agent tools.
bridge_ask for one question, bridge_dialog for a bounded multi-turn conversation between the two agents.
- Authenticated transport. NDJSON over TCP; PSK proved by HMAC-SHA256 challenge-response, every frame carries an HMAC over its sequence number. Replays and reordering are refused.
- One dedicated session per peer. A bridged agent never reaches the operator's own conversations.
- Read-only by default. A bridged child may
read / search / fetch; writes and command execution are refused by both a name filter and a tool-kind fence.
- Loop guards. Hop limit (default 8) and a per-minute turn budget (default 30) are enforced on every inbound turn; a bridged child cannot call
bridge_ask back.
- Reply language. Agent answers come back in the configured language (default
zh), while the transcript keeps what was actually typed.
- Persistent transcript. Every link event and turn is appended to
bridge-transcript.jsonl next to the profile, so the panel survives restarts.
- Self-diagnosis. On load the plugin writes
bridge-boot.json (version, services, effective tool posture) — the only way to see a partially failed setup in the desktop build, which keeps no application log.
Requirements
- Two machines that can reach each other on a private network (Tailscale, WireGuard, LAN + firewall rules, …).
- DeepSeek Harness desktop or web build with a profile (tested against
0.2.1-rc.2).
- Node.js ≥ 22.
- One pre-shared key of at least 16 characters, identical on both sides.
Install
On both machines.
1. Put the package in the profile
# Windows: profile lives at C:\Users\<you>\.dsh\profiles\desktop
$profileDir = "$env:USERPROFILE\.dsh\profiles\desktop"
New-Item -ItemType Directory -Force "$profileDir\node_modules" | Out-Null
Copy-Item -Recurse .\dsh-plugin-agent-bridge "$profileDir\node_modules\dsh-plugin-agent-bridge"
2. Register the plugin row
Append to $profileDir\cordis.patch.yml. The two machines differ only in nodeId and peer.
Machine A — the listener (accepts links):
- insert:
- id: agent-bridge
name: dsh-plugin-agent-bridge
config:
nodeId: machine-a
psk: !!js process.env.DSH_BRIDGE_PSK
listen: true
port: 19388
cwd: C:\Users\<you>\.dsh\bridge-workspace
tools:
preset: readonly
deny:
- ask_user_question
Machine B — the dialer (connects to A, retries with backoff up to 30 s):
- insert:
- id: agent-bridge
name: dsh-plugin-agent-bridge
config:
nodeId: machine-b
psk: !!js process.env.DSH_BRIDGE_PSK
peer:
host: 100.64.0.2 # A's address on your private network
port: 19388
cwd: C:\Users\<you>\.dsh\bridge-workspace
tools:
preset: readonly
deny:
- ask_user_question
psk may also be written literally, but the environment variable is safer: the profile file is easy to commit by accident.
3. Provide the pre-shared key
Set the same DSH_BRIDGE_PSK for the user that starts the app:
[Environment]::SetEnvironmentVariable('DSH_BRIDGE_PSK', '<at-least-16-characters>', 'User')
Then restart DeepSeek Harness. The plugin is host-side code; a page reload is not enough.
4. Check that it came up
- Sidebar shows Bridge below Automation tasks; opening it shows
machine-a ⇄ machine-b.
bridge-boot.json in the profile directory reports "phase": "complete" and the installed version.
- On the dialer,
peers becomes non-empty within ~30 s of the listener being reachable.
Configuration
| Key | Default | Meaning |
|---|
nodeId | required | This machine's name; peers address each other by it. |
psk | required | Pre-shared key, ≥16 chars; falls back to DSH_BRIDGE_PSK. |
bridgeId | dsh-agent-bridge | Scopes the key; two independent bridges may share a PSK. |
listen | true when peer.host is unset | Bind the listener. |
host | 0.0.0.0 | Listener bind address. |
port | 19388 | Listener port (not DSH's own web port). |
peer.host / peer.port | — / 19388 | Set peer.host to dial that address. |
tools.preset | readonly | none, readonly, or full. |
tools.allow / tools.deny | preset / ['ask_user_question'] | Name-level allow list and a deny list that always subtracts last. |
tools.allowKinds | read-only kinds | Coarser fence: read, search, fetch, other. |
exposeTool | true | Register bridge_ask / bridge_dialog. |
chat | true | Serve the operator routes (/bridge-chat, /state, /send). |
replyLanguage | zh | Language a peer's answer should come back in; '' disables. |
maxHops | 8 | Recursion limit on relayed messages. |
|
Using it
- Operator page. Sidebar → Bridge. Type and press Enter (Shift+Enter for a newline). Local-side turns are boxed; the peer's answers are body copy. Every message has a copy control on hover, and fenced code blocks have one in their header.
- From an agent. The
bridge_ask tool asks a peer one question; bridge_dialog runs a bounded dialogue. Both are refused inside a bridged child, so two agents cannot ask each other forever.
- From host code.
ctx.bridge.prompt(peer?, text, options) and ctx.bridge.converse(...).
Security model — read before exposing it
- The private network is transport, not identity. A VPN encrypts the link and fixes addresses; it does not decide who may connect. Anyone on that network holding the PSK can drive the agent on this machine.
- The PSK is the whole authentication. It never crosses the wire; both sides prove possession through an HMAC challenge-response, and every later frame is MAC'd and sequence-checked.
- The listener binds
0.0.0.0 by default. Put it behind a private network or firewall rules; do not expose port 19388 to the internet.
- Read-only is the default posture.
readonly blocks writes and command execution through both a name filter and a tool-kind fence. A bridged child cannot be widened by the peer.
- The bridged agent runs with the host process's permissions. Tool policy is the only boundary; treat a peer as code running on your machine.
ask_user_question is denied by default, so a peer cannot raise prompts on your screen.
Testing
npm test
201 tests: transport handshake/framing, reconnect, transcript persistence, tool policy and kind fence, recursion guards, chat routes and same-origin checks, reply language, and the client bundle's slot registration and rendering.
Troubleshooting
| Symptom | Likely cause |
|---|
authentication failed / bridgeId mismatch | PSK or bridgeId differs between the machines. |
peer "machine-b" is not connected | Dialer has not connected yet, or the listener is down. |
| Sidebar has no Bridge row | Client bundle not loaded; restart the app, then check bridge-boot.json (phase, version). |
| Bridge page appears but sends nothing | No connected peer: the composer disables itself and the header dot stays grey. |
| Agent answers in the wrong language | Check replyLanguage; it is appended to the prompt on the wire, so both sides must send it. |
| Page renders as plain text / cannot scroll | An old stylesheet survived a hot reload; reload the window, or restart. |
Version
0.10.8 — see CHANGELOG.md.
License
MIT.
中文
English | 中文
在私有网络上的两台 DeepSeek Harness 主机之间,建立双向 agent 到 agent 的桥。一台机器上的 agent 可以向另一台机器的 agent 提问,得到由对方用自己的工作区和工具产生的流式回答,还能进行多轮对话——全程走同一条已认证的 TCP 链路。
本项目与 DeepSeek 官方无关,是运行在 DSH profile 里的社区插件。
功能
- 双向链路:任一侧都能发起一轮,两个方向共用一条 socket。
- GUI 里的操作者页面:侧栏「自动化任务」下方有 Bridge 入口,点开是全宽对话页:连接状态、转录、Markdown、带复制的代码块,以及直接发往对端 agent 的输入框。
- Agent 工具:
bridge_ask 问一句;bridge_dialog 让两个 agent 做一次有界多轮对话。
- 认证传输:TCP 上的 NDJSON;PSK 通过 HMAC-SHA256 挑战应答证明,之后每一帧都带序号 HMAC,重放与乱序会被拒绝。
- 每对端独立会话:被桥接的 agent 永远碰不到操作者自己的会话。
- 默认只读:被桥接的 child 只能
read / search / fetch;写文件和执行命令会被「名称白名单 + 工具种类围栏」双重拒绝。
- 防环:每个入站回合都有跳数上限(默认 8)和每分钟预算(默认 30);被桥接的 child 不能再调
bridge_ask 回问。
- 回复语言:对端回答默认用配置的语言(默认
zh),而转录保留你实际输入的原文。
- 转录持久化:每条链路事件与对话都追加到 profile 旁的
bridge-transcript.jsonl,重启后面板不会清空。
- 自诊断:插件加载时写
bridge-boot.json(版本、各服务可用性、实际生效的工具姿态)——桌面版不落盘应用日志,这是从进程外看「装了一半」的唯一手段。
环境要求
- 两台能通过私有网络互通的机器(Tailscale / WireGuard / 局域网 + 防火墙规则均可)。
- 带 profile 的 DeepSeek Harness 桌面版或 Web 版(在
0.2.1-rc.2 上验证)。
- Node.js ≥ 22。
- 一条至少 16 字符的预共享密钥,两台机器必须一致。
安装
两台机器都要做。
1. 把包放进 profile
# Windows:profile 在 C:\Users\<you>\.dsh\profiles\desktop
$profileDir = "$env:USERPROFILE\.dsh\profiles\desktop"
New-Item -ItemType Directory -Force "$profileDir\node_modules" | Out-Null
Copy-Item -Recurse .\dsh-plugin-agent-bridge "$profileDir\node_modules\dsh-plugin-agent-bridge"
2. 注册插件行
追加到 $profileDir\cordis.patch.yml。两台机器只有 nodeId 和 peer 不同。
机器 A —— 监听方(接受连接):
- insert:
- id: agent-bridge
name: dsh-plugin-agent-bridge
config:
nodeId: machine-a
psk: !!js process.env.DSH_BRIDGE_PSK
listen: true
port: 19388
cwd: C:\Users\<you>\.dsh\bridge-workspace
tools:
preset: readonly
deny:
- ask_user_question
机器 B —— 拨号方(连 A;断线后按退避重试,上限 30 秒):
- insert:
- id: agent-bridge
name: dsh-plugin-agent-bridge
config:
nodeId: machine-b
psk: !!js process.env.DSH_BRIDGE_PSK
peer:
host: 100.64.0.2 # A 在私有网络上的地址
port: 19388
cwd: C:\Users\<you>\.dsh\bridge-workspace
tools:
preset: readonly
deny:
- ask_user_question
psk 也可以直接写明文,但用环境变量更安全:profile 文件很容易被误提交。
3. 设置预共享密钥
给启动应用的同一个用户设置相同的 DSH_BRIDGE_PSK:
[Environment]::SetEnvironmentVariable('DSH_BRIDGE_PSK', '<至少 16 个字符>', 'User')
然后重启 DeepSeek Harness。这是宿主侧代码,刷新页面不够。
4. 确认起来了
- 侧栏「自动化任务」下方出现 Bridge;点开显示
machine-a ⇄ machine-b。
- profile 目录里的
bridge-boot.json 显示 "phase": "complete" 与已安装版本。
- 拨号方在 A 可达后约 30 秒内
peers 非空。
配置项
| 键 | 默认值 | 含义 |
|---|
nodeId | 必填 | 本机名字;对端用它寻址。 |
psk | 必填 | 预共享密钥,≥16 字符;缺省时读 DSH_BRIDGE_PSK。 |
bridgeId | dsh-agent-bridge | 密钥作用域;两套独立桥可以共用一条 PSK。 |
listen | 未设 peer.host 时为 true | 是否监听。 |
host | 0.0.0.0 | 监听地址。 |
port | 19388 | 监听端口(不是 DSH 自己的 Web 端口)。 |
peer.host / peer.port | — / 19388 | 设置 peer.host 即成为拨号方。 |
tools.preset | readonly | none / readonly / full。 |
tools.allow / tools.deny | preset / ['ask_user_question'] | 名称白名单与「最后生效」的黑名单。 |
tools.allowKinds | 只读种类 | 更粗的围栏:read、search、fetch、other。 |
exposeTool | true | 注册 bridge_ask / bridge_dialog。 |
chat | true | 提供操作者路由(/bridge-chat、/state、/send)。 |
replyLanguage | zh | 对端回答使用的语言;'' 关闭。 |
maxHops | 8 | 中继消息的递归上限。 |
maxTurnsPerMinute | 30 | 每对端每分钟回合预算(60 秒滑动窗口)。 |
maxDialogRounds / dialogMaxMs | 50 / 600000 | 一次 bridge_dialog 的轮数与时长上限。 |
使用
- 操作者页面:侧栏 → Bridge。Enter 发送,Shift+Enter 换行。本地侧的消息带气泡,对端回答是正文排版;悬停消息可复制,代码块头栏也有复制按钮。
- 从 agent 调用:
bridge_ask 问一句;bridge_dialog 做一次有界多轮对话。两者在被桥接的 child 里都会被拒绝,避免两个 agent 无限互问。
- 从宿主代码调用:
ctx.bridge.prompt(peer?, text, options) 与 ctx.bridge.converse(...)。
安全模型——暴露到网络前必读
- 私有网络是传输,不是身份。VPN 只负责加密和地址;谁持有 PSK,谁就能驱动本机 agent。
- PSK 就是全部认证。它从不过线;双方用挑战应答证明持有,之后每帧都带 MAC 与序号校验。
- 监听默认绑
0.0.0.0。请放在私有网络或防火墙规则之后,不要把 19388 暴露到公网。
- 默认只读。
readonly 同时用名称过滤和工具种类围栏挡住写与执行;对端无法把围栏改宽。
- 被桥接的 agent 以宿主进程权限运行。工具策略是唯一边界,请把对端当作在本机运行的代码。
- 默认 deny
ask_user_question,对端不能在你屏幕上弹问题框。
测试
npm test
201 个用例:握手与分帧、重连、转录持久化、工具策略与种类围栏、递归保护、聊天路由与同源校验、回复语言,以及客户端 bundle 的槽位注册与渲染。
常见问题
| 现象 | 可能原因 |
|---|
authentication failed / bridgeId mismatch | 两台的 PSK 或 bridgeId 不一致。 |
peer "machine-b" is not connected | 拨号方还没连上,或监听方没起来。 |
| 侧栏没有 Bridge | 客户端 bundle 没加载;重启应用后看 bridge-boot.json(phase、version)。 |
| 页面能开但发不出去 | 当前没有已连接的对端:输入框会禁用,头部圆点保持灰色。 |
| 回复语言不对 | 检查 replyLanguage;它由发送方追加到线上 prompt,两侧都要配。 |
| 页面像纯文本、不能滚动 | 旧的 <style> 在热更新后残留;刷新窗口或重启应用。 |
版本
0.10.8 —— 见 CHANGELOG.md。
许可证
MIT。