WebRTC 构建机器与数据通道
构建机器是真正运行你构建的组件。它直接和你的浏览器或 MCP 客户端 讲 WebRTC,在每个通道上强制执行基于许可的权限,在沙箱里跑构建本身。
一台构建机器的解剖
一台构建机器是一台带以下东西的 Linux 主机:
aegis-agentsystemd 服务- 工具链 store(装着已安装的 IDF 版本和工具链)
- 沙箱运行环境
- 到构建服务器的出站 HTTPS 访问(无需入站端口)
- 一个能作为 peer 的 WebRTC 栈
重要的是,构建机器永远不为构建流程开放入站 TCP 端口。它向构建服务器 建立出站连接来定期查看任务,然后通过构建服务器提供的 ICE 服务建立到 客户端的出站 WebRTC peer 连接。
定期查看任务
构建机器跑一个循环:
每 2 秒:
GET ${CONTROL_BASE_URL}/agents/jobs?agent_id=...
对每个新任务:
本地验证许可签名
如果许可中的 peer 证书指纹匹配我们即将收到的 offer:
用配置的 ICE 服务打开 WebRTC peer 连接
按许可中允许的通道协商通道
运行构建,流式传送结果
这个循环在 INFO 级别是静默的 —— 只有错误会被记录。这是有意的;
聒噪的日志难读。要看查看活动,运维设 RUST_LOG=debug。
三个数据通道(服务端强制执行)
当 peer 连接协商通道时,构建机器强制执行:
- 通道名白名单 —— 只有许可中
allowed_channels列出的通道会被 接受。任何不在白名单里的通道,构建机器在ondatachannel时立即拒绝 (并关闭)。 - 每通道的 handler ——
espctl、pty、firmware各有一个专用 handler,知道消息格式并产生结构化事件。即使许可允许了未知名字, 也会被拒(因为没有 handler)。 - 带宽限制器 —— 每通道的滑动窗口字节计数器,按许可配置。 超过预算的突发会触发反压(写开始阻塞),而不是断开连接。
- 消息率限制器 —— 同样的形状,但计的是消息而不是字节。用来 防御那些发大量小消息的病态紧密循环。
构建怎么跑
espctl 通道一旦打开,构建机器收到 BuildRequest 消息后:
- 构建机器在
/var/lib/aegis/workspace/{job_id}/下创建一个工作区。 - 如果请求包含
project_bundle(一段 base64 编码的 git bundle, ≤ 50 MB),构建机器把它写到临时文件,在沙箱外跑git clone <bundle 文件> {workspace}/src。 - 构建机器准备一个干净的沙箱配置:
- 把
{workspace}/src读写挂载 - 把 store 中相关的 IDF 版本只读挂载
- 挂载一个小的可写
/tmp用作构建临时空间 - 丢弃所有 capabilities,禁止网络访问,禁止新挂载
- 把
- 在沙箱里,构建机器跑
idf.py build(或者配方指定的命令)。 - 编译进行中,构建机器读取子进程的 stdout 和 stderr,把行复用到
pty通道上作为原始字节,在espctl通道上发送结构化的PipelineEvent消息(例如“phase: compiling, progress 0.42“)。 - 构建结束后,构建机器从工作区读取结果
.bin文件,对内容计算 SHA-256,把字节作为 chunk 通过firmware通道送回(后面跟着一个 最终 manifest 消息,包含 SHA-256 和总大小)。 - 在可配置的延迟之后或者 peer 断开时,构建机器清理工作区。
线缆格式
espctl 通道上的消息默认对浏览器客户端是 JSON,对原生 Rust 客户端
是 bincode 编码的。构建机器从第一个字节自动检测编码。schema 在
aegis-proto crate 里;在 minor 版本之间稳定。
pty 通道是原始字节 —— 没有帧化,构建机器不加任何转义码。子进程写
到 TTY 上的任何东西都进通道。
firmware 通道使用一个微小的分块帧化:一个 header 消息声明
num_chunks + total_size + sha256,后面跟 N 个原始二进制 chunk。
数据队列上限和吞吐量
如果你在调性能,有一个微妙的点值得知道:
WebRTC 数据通道有一个可配置的每通道发送队列。
生产构建机器把那个队列上限设为 128 KB(#[cfg(test)] 构建用
128 MB 以避免阻塞单元测试,这可能误导随便做的基准测试)。
在 500 ms RTT 的 TURN 中继连接上,大致是:
128 KB / 500 ms = 256 KB/s 有效吞吐量
…对日志流和小固件镜像够用,但在大 *.bin 文件(~1 MB 起)上你
会注意到。无 TURN 中继的直连点对点连接显著更快。
失败模式
ICE 永远不收敛 —— 数据通道永远不打开。构建机器的 on_open handler
永远不触发,但 peer 连接状态在 ~5 秒后转到 Failed。客户端总应该
实现一个快速失败,在等待 on_open 的同时监视 Failed/Disconnected/
Closed 状态。
沙箱启动失败 —— 沙箱拒绝启动(缺 capability、宿主侧 seccomp
问题)。构建立即失败,在 espctl 通道上发结构化错误;数据通道保持
打开,客户端能读到。
构建进程超内存 —— 沙箱的内存 cgroup 杀掉子进程。构建机器在 结构化错误中报告这是一次构建失败,带 OOM 信号。数据通道保持打开。
许可在构建中过期 —— 构建机器在过期后拒绝颁发新的许可,但飞行
中的构建会跑完。构建服务器不会试图回溯撤销许可。如果你需要构建可
中断,用 build.cancel。