Wire 协议参考

(这一页是给要从代码接进 daemon、或者想搞明白管道上到底跑什么字节的人看的。只用图形界面或 CLI,可以跳过。)

Conmux 的 daemon 和它的客户端(CLI、GUI、你自己写的程序)之间,在一条命名管道上收发一串 JSON 帧。这一页把这串帧的形状写清楚——每一条都对应 crates/conmux/src/protocol.rs#[derive(Serialize, Deserialize)] 的真实类型,不是另起炉灶的文档。

稳定性提醒:协议层是 conmux 唯一承诺稳定的公开面(serde 形状即契约,破坏性改动走 minor + CHANGELOG)。协议层之外的 API 都还不稳定。下面每个类型名都能在 protocol.rs / event.rs 里一一对上。

一条连接上跑什么:帧信封 WireFrame

所有帧都裹在一个信封枚举 WireFrame 里,externally tagged(JSON 里是 {"变体名": {...}})。一共五种:

方向载荷
Hello客户端 → daemonprotocol_version: u32client_kind: String
HelloAckdaemon → 客户端protocol_version: u32daemon_version: String
Request客户端 → daemon一个 MuxRequest
Replydaemon → 客户端一个 MuxReply
Notifydaemon → 客户端一个 MuxNotify(异步事件,无相关号)

方向是有约束的,不是随便发:daemon 只接受 Hello(且仅在握手期)和 Request;客户端只接受 HelloAck / Reply / Notify。发反了 = 协议错误,直接断连。

握手先行:客户端连上后首帧必须是 Helloclient_kind 只是个自由标签,进审计日志,不参与任何授权判定——别指望靠它提权。

版本必须严格相等

PROTOCOL_VERSION 当前是 1,它独立于 crate 版本。握手时 daemon 拿自己的版本和 Hello 里的 protocol_version严格相等校验——不是"大于等于",是"必须一样"。对不上就不给你 HelloAck。任何 wire 形状的破坏性变更都要 bump 这个常量。

拒收未知字段(deny_unknown_fields

WireFrame 信封、Hello / HelloAck、以及 MuxOp 都开了 deny_unknown_fields:报文里多一个没定义的键,反序列化直接失败,而不是静默忽略。这条最要紧的用途是把注入源挡在门外——见下面 Send

请求:MuxRequest 与操作枚举 MuxOp

一个请求 = 相关号 + 一个操作:

MuxRequest { correlation_id: u64, op: MuxOp }

correlation_id 用来把应答和请求配对(应答会带回同一个号)。MuxOp 是全部能下达的操作,逐一列在下面。右列是这个操作成功时对应的 MuxReply::Ok 载荷(MuxPayload 的变体)。

MuxOp 变体字段成功载荷(MuxPayload备注
SpawnSpawnRequestSpawned(PaneId)起一个新 pane
RespawnSpawnRequestSpawned(PaneId)原子同 ID 重起(消掉 KillTree+Spawn 之间的 ID 复用窗口)
Sendpane_iddata(见下)Sent向 pane 注入输入
CaptureCaptureRequestCaptured(CaptureResult)抓 scrollback
Resizepane_idsize: PaneSizeResized改 pane 行列
KillTreepane_idKilled杀掉整棵进程树
ListPanesPanes(Vec<PaneState>)列出所有 pane
Subscribepane_idSubscribed订阅该 pane 的事件流
Unsubscribepane_idUnsubscribed取消订阅
Attachpane_idAttachSnapshot { ... }原子「订阅 + 快照」
ListThemesThemes(Vec<TerminalTheme>)列出主题预置
SetThemeid: StringThemeSet热切换主题;另广播 ThemeChanged
KillServerServerKillScheduled终结 daemon 及全部会话
PinExecutablepath: StringPinnedpin 可执行文件到信任库
UnpinExecutablepath: StringUnpinned移除 pin

这就是全部 15 个变体,一个不多一个不少。MuxOp 故意不加 #[non_exhaustive]:新增变体是一次显式的 minor 决策,daemon 的 dispatcher 对它穷尽 match,未来加变体会在 daemon 侧编译报错,逼你显式处理,而不是静默漏掉。

关于 Send:注入源不过 wire

Senddata原始字节,上 wire 时经 base64 编码(方向键、Alt 组合、二进制粘贴这些非 UTF-8 内容用普通字符串无法无损携带)。

注意 Send 上没有 source 字段,这是故意的:注入来源由接收端边界按信道身份赋值,不允许客户端在 wire 上自报。配合 deny_unknown_fields,一条带 source 键的 Send 报文会反序列化即失败——拒收,而不是接受后再丢弃。

关于订阅类操作

Subscribe / Unsubscribe 维护「这条连接关心哪些 pane」的集合,daemon 的 fan-out(FanoutSink)据此把 PaneOutput / PaneExited 事件只投给订阅了的连接Attach 是一步到位的「订阅 + 原子快照」:先注册订阅、再取当前 scrollback 快照,之后按 seq > last_seq 连续喂 live 流——中间不丢帧不重帧(限速 + per-pane 并发=1 防快照放大)。这套「fan-out 分发 + 无缝快照拼接」在当前 daemon 里已经实现并有测试(M2 里程碑已落地,见 daemon.rs::attach_with_limits / FanoutSink)。(SetTheme 明确不持久化主题偏好——持久化归上层消费者 / GUI 壳,不归 daemon。)

应答:MuxReplyMuxPayload

应答两条路,都带 correlation_id 供配对:

MuxReply::Ok  { correlation_id: u64, payload: MuxPayload }
MuxReply::Err { correlation_id: u64, error:   ConmuxError }

出错走 Err,携带的是 ConmuxError机制层错误,比如 PaneNotFound)——conmux 只报它机制层的错,不认 conflux 的语义错误。

成功载荷 MuxPayload 的变体已在上表右列逐一对应。多数是无字段的确认(Sent / Resized / Killed / Subscribed / …)。两个带真数据的值得单独看:

  • AttachSnapshot——Attach 的原子快照,字段:mode_preamble_b64(终端模式前导,如 alt-screen)、history_b64(scrollback 历史)、last_seq(末序号)、pane_state。客户端重建 = 喂 preamble → 喂 history → 之后按 seq > last_seq 连续喂 live 流,不丢不重。
  • Captured(CaptureResult)——Capture 的结果,含 base64 数据、首/末绝对行号、是否截断、是否"实际已满"。

MuxPayload 加了 #[non_exhaustive](与 MuxOp 相反),消费者 match 时要留 _ => 分支。

异步事件:MuxNotify

daemon 主动往客户端推的事件,裹在 WireFrame::Notify 里,没有相关号(它不回应任何请求)。定义在 event.rs,三种:

MuxNotify 变体字段含义
PaneOutputpane_idseq: u64data(base64)pane 的原始输出;seq 是 per-pane 单调序号,供重放对账
PaneExitedpane_idexit_code: Option<i32>进程退出;拿不到退出码时是 None不伪装成 0
ThemeChangedid: String主题被 SetTheme 切换后广播,供实时换肤

SendPaneOutput.data 上 wire 也走 base64(原始字节可能含不可打印 / 非 UTF-8 内容)。in-proc 直连的消费者(sink 实现)仍收原始 Vec<u8>,base64 只在管道边界生效。

seq 的单调性有个硬纪律:如果消费方或 conmux 对 PaneOutput 做合帧,只能拼接、不得丢字节、seq 必须连续——丢帧会让消费方据残缺输出做错误决策。

诚实边界MuxNotify 只发 conmux 机制层确知的事件——字节、退出、换肤。它不发"这个 agent 在思考 / 在等权限"这类语义状态:那是上层(比如 Conflux)对 PTY 内容的解读,不属于协议层。别指望在 wire 上直接读到 agent 状态。

一眼核对

想快速验证上面这些,protocol.rs 底部的 #[cfg(test)] 模块就是活文档:all_ops_round_trip 列全了 15 个 op,wire_frame_all_directions_round_trip 跑五种信封帧,send_with_source_field_is_rejected_on_wirehello_rejects_unknown_fields 演示了拒收未知字段的两处。改协议前,先让这些测试红,是最稳的对账方式。