设备¶
环境服务器拥有模拟器和仿真器的方式与它拥有终端的方式相同:发现、流传输和 agent 访问都在那里运行,每个客户端都通过环境连接到达它们。这就是让设备面板能够通过 Tailscale 和 T3 Connect 工作的原因,包括当 SSH 主机运行设备时。
两个外部工具,一条接缝¶
expo-device-hub 负责流传输,agent-device 负责驱动。两者都以固定版本通过 npm 安装到 T3 主目录中,在其对应的设备面板同意步骤之后。手动设置只安装并启动 expo-device-hub;agent-device 在获得 agent 访问授权之前保持缺失和停止状态。两者都使用服务器的 Node 运行;npx 会使重启后的首次 device_open 依赖注册表。hub 是一个受监管的子进程,而不是导入的中间件,因为 serve-sim 通过原生插件加载私有 CoreSimulator 框架,而那里的崩溃绝不能导致服务器宕机。
所有平台特定的内容都位于 DeviceHost 之后。该服务通过主机的设备操作列出、启动和关闭设备,而代理和 MCP 工具只能看到 hub 源和 agent-device 端点。运行 expo-device-hub 的主机共享同一套基于 hub 的这些操作实现,因此设备位于其他地方的主机只需替换它们。SSH 主机将两个端点都转发到服务器的环回地址。每个代理请求还携带主机 id;仅凭设备 id 在不同主机之间并不唯一。
EAS Simulator 主机属于计费会话¶
EAS 主机没有机器:每个打开的设备都是一个 EAS Simulator 会话,该会话会向 Expo 账户计费,直到其停止。每个会话都按主机的运行和空闲限制创建;主机拒绝在超过其会话限制后启动;关闭主机会停止其会话。它启动的会话记录在状态目录下,如果此服务器崩溃,下一个服务器会停止它们;其他工具在同一账户上启动的会话则永远不会被触碰。
每个会话都带有自己的预览主机(serve-sim,或用于 Android 的 serve-emu)和 agent-device 守护进程,并带有各自的令牌。代理请求主机为每个请求路由(DeviceHostReady.upstream),并在服务器端添加会话令牌,因此客户端永远不会持有它。agent 配置文件按设备写入(DeviceOperations.agentEndpoint),这就是为什么 device_open 会在定位设备之前先打开设备。设备设置和工具在设备机器上运行 simctl 或 adb,而 EAS 不暴露这些,因此在那里不可用。
Simlock 主机通过 agent-device 前端租用¶
Simlock 主机(alpha)通过针对网关的 agent-device 前端运行 T3 自己的 agent-device CLI 来打开设备(先 connect proxy,再 open),而不是使用 Simlock 的租约 HTTP API:agent-device 无法采用它未分配的租约。租约绑定到那一个 agent-device 会话以及 open 运行所在的目录,因此 agentEndpoint 为每个线程提供相同的会话、一个保存其连接记录的 stateDir,并且不带 --udid/--serial(网关拒绝选择器)。安装只能从该目录进行;device_open 会指定该目录。
Simlock 在两个平台上以 serve-sim 的格式向持有者提供租约的屏幕和输入:AVCC 信封位于 /v1/leases/<id>/stream,serve-sim 的输入消息位于 /v1/leases/<id>/input,两者都带有 agent 令牌。因此客户端将 Simlock Android 租约视为 iOS(在流客户端中为 serveSimOnly),并且 upstream 将 serve-sim 的 stream.avcc 和 helper/ws 路径映射到这些路由。Simlock 没有 MJPEG、截图或主机命令路由,因此面板在那里只提供输入。
租约在被跟踪期间会续期,并在主机期限限制到达时释放;崩溃的服务器遗留的租约会在一个 TTL 后自行结束,因此没有账本。
hub 永远不会暴露¶
serve-sim 有一个 shell-exec 路由,其令牌可从它自己的未认证 /api 读取,而 serve-emu 的操作路由根本没有认证。hub 绑定到环回地址,唯一的入口是代理,它将流、配置和截图路由列入白名单,并将每个请求认证为环境会话。<img> 和 WebSocket 无法携带标头,因此代理像 /ws 升级一样进行认证:cookie,或持有者(bearer)和 DPoP 客户端通过已认证 HTTP 签发的短期 wsTicket。请求到达 hub 之前,该票据会被剥离。
流响应带有 Cache-Control: no-transform;否则压缩中间件会缓冲一个永不结束的 MJPEG 正文。浏览器只有在下一部分开始后才会绘制一个 MJPEG 部分,而 hub 只在屏幕变化时写入一个部分,因此代理在短暂的静默期后会重新发送最后一部分。在浏览器开发中,Vite 代理必须转发 /api 的 WebSocket 升级,而不仅仅是 /ws。
设备设置永远不会经过 hub¶
serve-sim 的预览通过同一个 exec 通道发送 shell 命令来驱动其“工具”面板。对其实行代理,即使将其列入白名单,也会让任何环境会话在主机上获得任意命令执行能力,因此 T3 不这样做。device.action RPC 本身通过 DeviceHostReady.run 运行底层的 simctl、adb 和 serve-sim 辅助二进制文件,每个控件对应一个类型化操作,并返回它读取到的设置。代理白名单只随读取路由(无障碍树、前台应用、事件日志)增长,并且在除截图捕获和流调优之外的所有地方都拒绝非 GET 方法。
代理通过 CLI 驱动设备¶
device_* 工具包刻意只包含四个工具:list、open、screenshot 和 close。设备驱动通过 agent-device CLI 完成,该 CLI 具备代理所需的语义快照模型,并随着自身发布始终保持最新。T3 会在提供方的 PATH 之前预置一个 shim 目录。即使环境服务器无法运行模拟器,CLI 也会安装到该服务器上。主机按需启动。
该环境在提供方子进程生成时即已固定,因此 prepareMcpSession 只有在设备支持和代理访问均已启用、会话具备 device 能力、且机器至少能运行一个平台时,才会启动 agent-device。如果之后才从 device_open 启动它,会让已在运行的代理缺少 CLI。
如何驱动设备由 device_open 返回,而不是保存在始终加载的提示词或技能中:对于从不打开设备的线程,这不会产生任何开销,也不会偏离固定的 CLI 版本。device_open 返回一个绑定到其 agent-device 会话的启动器,因此代理无需目标标志。启动器会省略对应命令不接受的标志(cdp 与 Metro 通信,而非与设备通信),拒绝那些会替换 T3 连接的命令,并首先检查该连接:agent-device 会把沙盒 shell 和断开的隧道一律报告为不可达的守护进程,因此启动器会指明具体是哪种情况。结束会话(device_close、Device 面板、关机)也会关闭其 agent-device 会话,从而退出该会话打开的应用,并释放 CLI 对设备的占用。
代理在打开任何设备之前会在这些工具与 ssh 之间做出选择,因此持有 device 能力的会话会通过其适配器的指令通道,从 orchestrationInstructions.ts 获得一段简短说明。这段说明将本地和 SSH 主机上的模拟器 UI 操作指向这些工具;simctl、adb 和 ssh 仍可用于这些工具未覆盖的场景。Pi 和 Muse 目前还不会发送这段说明。Claude 的原生子代理永远看不到会话的系统提示词,因此 SubagentStart 钩子会将这段说明传递给它们——此时运行时执行钩子,且子代理的上下文并非隔离。
查看器解码两种内置协议¶
hub 内置了两个传输格式不同的流媒体服务器。iOS 视频是一个由 AVCC 封装构成的 HTTP 主体,使用 WebCodecs 解码,输入则通过独立的二进制 WebSocket 传输;Android 在单个 WebSocket 上多路复用 SEMU 帧格式的 H.264 和 JSON 手势。DeviceStreamView.tsx 同时支持这两种协议,因此一个面板即可覆盖两个平台。
WebCodecs 仅限安全上下文(secure context),因此查看器会探测 isConfigSupported;在 iOS 上若解码不可用,则回退到 MJPEG。Android 的 SEMU 套接字没有 MJPEG;此时面板会报告其无法解码。
serve-sim 也可以锁定为 WebRTC(例如在 EAS Simulator 中),此时会以 409 拒绝 AVCC,并在响应中指明传输方式。查看器仍然首先请求 AVCC,因此提供 AVCC 的 hub 无需额外付出;查看器会在收到该 409 时切换,或者在支持 RTCPeerConnection 的浏览器中其 MJPEG 图像加载失败时切换。Offer 不做 trickle 处理:serve-sim 接收一个包含全部候选项的 offer。video 元素只馈送到同一个 canvas 汇聚端,因此面板和移动端 WebView 共享同一个渲染表面。Android 在打开视频套接字之前会先查询 serve-emu 的 /api;当通告 WebRTC 时(EAS Simulator),Android 会流式播放 WebRTC,并保留一个独立的 video=0 套接字用于输入,这样输入可以重新连接而无需重启视频。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code