当前协议说明
这一页只讲协议层:谁和谁说话,走哪条链路,哪些内容由谁看见。适合放在 代码架构 和 安全设计 之间一起读。
1. 连接入口
agent 和 client 都通过同一个 WebSocket 入口连接 relay:
text
ws(s)://<relay-host>/ws?role=<agent|client>&token=<token>&deviceId=<deviceId?>role=agent:电脑上的 agentrole=client:手机或浏览器端token:agent token 或配对得到的 client access tokendeviceId:agent 连接时必须带;client 的 device scope 由 token 决定
2. 配对与授权
agent 侧
- agent 通过固定
TERMPILOT_AGENT_TOKEN接入 relay - agent 启动或申请配对码时,会确保本地存在长期 ECDH 密钥对
POST /api/pairing-codes会把agentPublicKey一起提交给 relay
client 侧
- 浏览器在兑换配对码前生成本地长期 ECDH 密钥对
POST /api/pairings/redeem会提交:pairingCodeclientPublicKey
- relay 返回:
deviceIdaccessTokenagentPublicKey
这样浏览器和 agent 就建立了同一条设备范围的密钥关系。
3. relay 可见与不可见的数据
relay 当前可见的服务端元数据只有:
- 一次性配对码
- 设备范围 access grants
- 审计事件
relay 当前不可见的会话内容包括:
- 会话标题
cwdshelltmuxSessionName- 会话状态细节
- 终端输出
- replay 缓冲
这些内容都只保留在 agent 所在电脑,并以加密信封方式经过 relay。
4. WebSocket 消息分层
明文系统消息
relay 仍会直接发送少量系统消息:
auth.okrelay.stateerror
这些消息只描述连接状态、设备在线情况和鉴权错误,不包含会话内容。
加密信封消息
会话相关消息不再以明文 session.* 直接经过 relay,而是包裹在两类信封里:
secure.clientsecure.agent
信封结构:
json
{
"type": "secure.client",
"reqId": "req_123",
"deviceId": "mac-d1f1c6cb",
"payload": {
"iv": "<base64>",
"ciphertext": "<base64>"
}
}其中:
payload.iv:AES-GCM IVpayload.ciphertext:浏览器或 agent 加密后的业务消息
relay 只根据 deviceId 和 accessToken 做路由,不解析加密载荷里的业务字段。
5. 业务消息
解密之后,浏览器与 agent 之间仍然使用统一的 session.* 业务消息:
client -> agent
session.listsession.createsession.inputsession.resizesession.killsession.replay
agent -> client
session.list.resultsession.createdsession.outputsession.statesession.exiterror
也就是说,现有会话协议还在,但它只存在于浏览器和 agent 的解密边界内。
6. 会话对象
当前 SessionRecord 字段包括:
siddeviceIdnamebackendlaunchModeshellcwdstatusstartedAtlastSeqlastActivityAttmuxSessionName
其中:
backend当前固定为tmuxlaunchMode当前可能是shell或command
7. 输出同步与 replay
当前输出模型不是终端字节流,而是 ANSI 快照替换。
agent 侧行为:
- 定时执行
tmux capture-pane -p -e -N -S -2000 - 如果 pane 内容发生变化,递增
lastSeq - 在本地缓存最近输出帧
- 向所有已配对 client 广播加密后的
session.output
replay 行为:
- client 进入会话或重连时,发送
session.replay - agent 用本地缓存的最近帧响应
- replay 不依赖 relay 内存缓冲
8. HTTP 接口
POST /api/pairing-codes
用途:由 agent 申请一次性配对码。
请求体:
json
{
"deviceId": "mac-d1f1c6cb",
"agentPublicKey": "<base64 spki>"
}POST /api/pairings/redeem
用途:由浏览器兑换配对码。
请求体:
json
{
"pairingCode": "ABC-123",
"clientPublicKey": "<base64 spki>"
}返回:
json
{
"deviceId": "mac-d1f1c6cb",
"accessToken": "<token>",
"agentPublicKey": "<base64 spki>"
}GET /api/devices/:deviceId/grants
用途:agent 拉取当前设备 grants,用于建立 access token 与 client 公钥的映射。
DELETE /api/devices/:deviceId/grants/:accessToken
用途:撤销某个已绑定 client 的访问权。relay 会主动断开对应 client WebSocket。
GET /health
返回当前 relay 健康状态。当前与安全相关的关键字段包括:
storeModesecurity.relayStoresSessionContentsecurity.endToEndEncryptionRequiredForPairedClients
9. 如何阅读这份协议
如果你正在排查具体问题,可以按这个思路读:
- 配对拿不到访问权:先看第 2 节和第 8 节
- 浏览器与 agent 是否真的在加密中转:看第 3 节和第 4 节
- 会话为什么能同步输出:看第 5 节到第 7 节
10. 当前协议边界
- 会话后端仍固定为
tmux - 输出同步仍是快照替换,不是字节流终端协议
- relay 仍然是中心路由点,但不再承载会话主数据
- 使用旧 access token 且缺少本地密钥绑定的 client,需要重新配对
- Web UI 当前由 relay 托管,配对与授权也通过 relay 建立