# DSH Cross-Session Relay > 中文 | [English](./README.md) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Weiyang742/dsh-cross-session-messaging/blob/main/LICENSE) [![CI](https://github.com/Weiyang742/dsh-cross-session-messaging/actions/workflows/ci.yml/badge.svg)](https://github.com/Weiyang742/dsh-cross-session-messaging/actions/workflows/ci.yml) 让多个独立运行的 DeepSeek Harness(dsh)会话互相发现、对等收发文本消息。 ## 解决的问题 dsh 内置了两种「跨会话」能力,但都不是对等的进程间通信: - **subagent(子代理)**:主从委托——父 agent 派生子代理,子代理活在父的上下文里、受父控制,不是独立的对等会话。 - **`@session` 引用**:Web 界面的单向只读召回,只能「引用」另一个会话的历史快照,不能「发消息」给它。 但在实际协作里,两个会话常常**需要互发消息**。典型的例子是「数据库助手」和「前端助手」被拆成两个独立会话,各自待在最适合自己的环境里: - 数据库助手跑在能连库的内网机器上,有数据库上下文和权限; - 前端助手跑在开发机上,负责写页面。 当前端助手需要「用户表的建表 SQL」时,它自己没有数据库的上下文和权限,只能**发消息去问**数据库助手;数据库助手查出来之后,再**回发**给前端助手。 这种「不同专长 / 不同权限 / 不同机器之间的对等问答」,正是 subagent 的主从结构和 `@session` 的只读召回都覆盖不了的场景——它们要么是父子控制,要么只能单向看。本插件补上的就是这条对等的双向通道。 ## 本插件怎么做 一条**对等、无中心、跨进程(可跨机)的消息通道**: - **模型主动**:agent 自己调用 `cross_session_list_agents` 发现对端、`cross_session_send_message` 发送,无需人手动转发。 - **对等注入、不授权**:消息以 `form: 'relay'` 身份注入接收方 agent,只作为文本,**不替用户批准任何工具调用**——安全边界仍在接收方自己的审批。 - **可靠投递**:发送方 spool 落盘 + 指数退避重试 + 死信;接收方先落盘再 ack;at-least-once + 内容去重。 - **本机 / 跨机一体**:同机走 Unix socket,跨机走 TCP+TLS(fail-closed,静态 peers)。 - **无中心**:磁盘 registry 即状态,没有中心 server。 ## 与 dsh 内置能力的区别 | | subagent | `@session` 引用 | 本插件 | |---|---|---|---| | 会话关系 | 主从 | 单向只读 | 对等 | | 能否互发消息 | 父 → 子控制 | 否 | 是,双向 | | 跨进程 / 跨机 | 同进程 | 同进程 | 是 | | 注入形态 | 子代理上下文 | 历史快照 | relay 文本,不授权 | ## 安装 前置:Node.js >= 24.11、一个可用的 dsh(DeepSeek Harness)环境。 从源码安装时先构建产物(`lib/` 不随仓库分发): ```sh npm install npm run build ``` 然后把插件装进一个 profile: ```sh dsh plugin --profile demo add ./cross-session-messaging dsh --profile demo --dump-config # 验证织入 ``` 插件通过 `cordis.patch.yml` 以 bundle 层插入 profile,未列出的配置走默认值。 ## 使用 装好后,开两个 dsh 会话(例如 Web 界面的两个「New Session」),在其中一个里用自然语言说: > 帮我看看当前还有哪些其他会话,给其中一个发条消息:你好。 模型会自己调用 `cross_session_list_agents` 发现对端、`cross_session_send_message` 发送;另一个会话会以 `form: 'relay'` 收到这条消息。人只下自然语言指令,不点任何工具名。 完整的端到端步骤见 [Quick start](docs/quickstart.md)。 ## 配置 配置写在插件的 `cordis.patch.yml` 的 `config:` 段里(安装时已经用它织入),未列出的字段走默认值。本机使用通常只需要默认值,需要定制的主要是 `name` 和入站策略: ```yaml - insert: - id: cross-session-messaging name: '@wy/dsh-cross-session-messaging' config: dirs: baseDir: ~/.dsh/cross-session # 数据目录根,registry/socket/spool 等派生自它 inbound: rules: [] # 入站规则,见下 defaultDecision: auto # 无规则命中时的兜底 name: '' # 本会话可读名,缺省为 shortId # 跨机(可选,见「跨机」): # remote: # listen: { host, port, identity, secret } # tls: { key, cert } # peers: # - { name, host, port, secret, tls, tlsCa/tlsFingerprint } ``` - `dirs.baseDir`:所有数据目录(registry / socket / spool / credential / deadletter / audit)的根,默认 `~/.dsh/cross-session`;六个子目录都可单独覆盖。 - `inbound.rules`:按 `receiver`(本地会话 id/name)与 `sender`(发送方 sessionId)匹配的 `accept | hold | refuse` 规则;两者省略分别为「所有本地会话」与「默认规则」。 - `inbound.defaultDecision`:无规则命中时的兜底;`auto`(默认)参考接收方 `DSH_PERMISSION_MODE`(`danger-full-access` → hold,其余 → accept),也可显式设 `accept | hold | refuse`。 - `name`:本会话的可读名,供 `cross_session_send_message` 按名寻址。 ### 跨机 跨机需要额外配 `remote`(本机互发不用管): - `remote.listen`:本机作为接收方时的 TCP 监听端点(`host` / `port` / `identity` / `secret`);`identity` 是这台机器在对端眼里的身份,写入入站消息的 `from`。 - `remote.tls`:监听证书(`key` / `cert`);省略则自动生成自签证书到 `spool/tls`,指纹写到 `spool/tls/fingerprint`。 - `remote.peers`:本机作为发起方时的静态对端列表(`name` / `host` / `port` / `secret` / `tls` / `tlsCa` 或 `tlsFingerprint`)。`tls` 缺省 `true`;跨机 TLS 必须提供 `tlsCa` 或 `tlsFingerprint`,否则拒绝连接。 ### 容量与重试(调优) `limits.*`(队列上限、消息大小、限流、去重窗口)、`delivery.*`(投递重试与退避)、`metricsIntervalMs`、`auditMaxBytes` 都有保守默认值,绝大多数场景不用改;需要调整时按字段名改即可。 ## 工具 - `cross_session_list_agents`:列出可达的 peer session(sessionId / name / shortId / cwd / pid)。 - `cross_session_send_message`:向指定 session 发送文本;目标可用 sessionId、shortId 或 name。 - `cross_session_inbox_review`:查看 hold 中等待 accept/refuse 的消息。 - `cross_session_inbox_resolve`:对 hold 消息执行 `accept` 或 `refuse`。 ## 权限语义 - peer 消息以 `source.form = 'relay'` 注入,只作为文本进入模型,**不会**替用户批准任何工具调用;接收方工具仍走自身的审批(默认 fail-closed)。 - 消息 `from` 由服务端鉴权覆盖(本地=sessionId、跨机=`listen.identity`),不信任发送方自报;本会话被权限系统拒绝过的工具名会被记住,`send_message` 不会替对方转发这类操作。 ## Hooks / 子进程回发 插件把当前 socket / sessionId / 握手 token 通过 `ctx.shellEnv` 注入 shell 子进程(`DSH_CROSS_SESSION_SOCKET` / `DSH_CROSS_SESSION_ID` / `DSH_CROSS_SESSION_TOKEN`)。长任务子进程或 hook 可连上 socket 回发文本,或调用导出的 `sendToCurrentSession(text)`。 ## 测试 ```sh npm test # 单元 + 集成(真实 socket) npm run check # lint + typecheck + build + test ``` 测试分层、跨机压测矩阵与 CI 策略见 [docs/testing.md](docs/testing.md)。 ## 限制 - 跨机发现是静态 `peers` 配置(需重启生效),无中心 registry,也不支持公网 NAT 穿透;真实公网双机已验证 transport/service 层(低频稳定、高频 ping 偶发),模型主动调工具的宿主端到端尚未验证。 - TLS 证书自动生成(openssl 自签),但未做过期轮换/吊销;跨机 TLS 必须配置 `tlsCa` / `tlsFingerprint`(fail-closed)。 - 投递为 at-least-once + 内容去重(含跨重启),非严格 exactly-once:窗口外或不同内容仍可能重复。 - accepted 与 held 均落盘(重启恢复);accepted 消息在注入后标记,未注入会重新投递,崩溃点极小。 - 跨机身份是单对端:`listen.identity` 为单值(两台机器假设),未来多对端需扩展为 `allowed` 列表。 - 转交防护是启发式(best-effort),真正的安全边界是 relay 不授权 + 默认入站策略。 - peer 消息以 `form: 'relay'` 注入,不携带用户授权;接收方工具仍走自身审批(默认 fail-closed)。