
OpenSandbox execd 交互式 PTY 会话WebSocket 驱动的终端、回放与多观察者协议解析【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandboxOpenSandbox 的 execd 组件沙箱内的执行守护进程内置了一套交互式 PTY 会话能力客户端通过 REST 创建会话、通过 WebSocket 附着到一个长生命周期 Shell即可获得与真实终端一致的 ANSI 彩色输出、stty、窗口尺寸调整resize等体验无需 TTY 时还可切换为 pipe 模式分离 stdout/stderr并支持断线重连回放replay、读/写持有者独占单 holder以及任意数量只读观察者viewer。读完本篇你能掌握该功能的完整 REST/WebSocket 协议、查询参数与二进制帧格式并能结合components/execd的 Go 源码理解 Shell 启动、回放缓冲、接管takeover与观察者隔离的底层实现。1. 能力定位与整体结构PTY 会话是 execd 对外暴露的一组 HTTP WebSocket 端点路由注册在 router.go 中POST /pty 创建 PTY 会话 GET /pty/:sessionId 查询会话状态running、output_offset DELETE /pty/:sessionId 服务端拆除会话 GET /pty/:sessionId/ws 附着到会话的 WebSocket 端点execd 默认监听 44772 端口见 flag/parser.goflag.IntVar(ServerPort, port, ServerPort, ...)默认值44772。从源码结构看整个功能由三层组成REST 控制层pty_controller.go 处理会话的创建/查询/删除只做参数校验与错误映射不直接管理进程WebSocket 协议层pty_ws.go 负责握手、holder/viewer 角色判定、帧收发与接管逻辑运行时层pty_session.go 中的ptySession真正管理 Shell 进程、PTY master fd、stdin 写入与输出广播并内嵌一个环形回放缓冲区 replay_buffer.go。平台限制PTY 会话仅支持 Unix/macOS/Linux。控制器入口会先调用runtime.IsPTYSessionSupported()做门禁在 Windows 实现pty_session_windows.go中该检查返回 false请求会被直接以501 Not ImplementedNOT_SUPPORTED错误码拒绝。2. 完整实战流程以下四步完整覆盖官方文档给出的典型用法。2.1 创建会话Shell 此时并不启动curl -s -X POST http://127.0.0.1:44772/pty \ -H Content-Type: application/json \ -d {cwd:/tmp} # → { session_id: id }请求体对应 model/pty.go 中的CreatePTYSessionRequest字段说明cwd可选Shell 的工作目录空则继承 execd 进程的工作目录command可选交给 Shell 以-c方式执行的自定义命令注意创建接口只生成一个会话对象并返回session_idShell 进程是在第一个 WebSocket 连接建立时才真正启动的。这一点在源码中体现得很明确StartPTY()/StartPipe()的注释都要求“Must be called with the WS lock held”而它们是由 WS 处理函数在握手成功后调用的见 pty_session.go 中ptySession生命周期注释Create → StartPTY/StartPipe → AttachOutput → 进程退出。2.2 打开 WebSocketws://127.0.0.1:44772/pty/session_id/ws默认是 PTY 模式。支持以下查询参数Query用途pty0使用 pipe 模式代替 PTYsinceoffset断线重连后从指定字节偏移开始回放偏移可取自GET /pty/:id的output_offsettakeover1驱逐当前持有者而不是收到409然后附着到同一个 Shell配合since可回放滚屏内容modeviewer在 Shell 已运行后以并发的只读观察者身份附着配合since回放历史输出2.3 数据流量协议连接的第一个 JSONconnected帧会标明本次连接的身份modepty或pipe与roleholder或viewer。二进制帧首字节为类型标记后续为原始字节首字节方向含义0x00客户端 → 服务端stdin 原始输入0x01服务端 → 客户端stdout 输出0x02服务端 → 客户端stderr 输出仅 pipe 模式0x03服务端 → 客户端回放帧8 字节大端偏移量 原始字节viewer 的保留输出与实时输出都用此帧这些常量与 model/pty_ws.go 中BinStdin 0x00、BinStdout 0x01、BinStderr 0x02、BinReplay 0x03的定义一一对应。JSON 文本帧用于控制类消息{type:resize,cols:120,rows:40} {type:signal,signal:SIGINT} {type:ping}客户端帧结构对应ClientFrametype/data/cols/rows/signal字段服务端帧对应ServerFrame含mode、role、exit_code、code、error等字段均在 model/pty_ws.go 中定义。2.4 一个读/写持有者 多个只读观察者同一会话只允许一个读/写 holder。第二个读/写连接会收到HTTP 409ALREADY_CONNECTED除非它带?takeover1当前 holder 会被以 WebSocket 关闭码4001reasonTAKEN_OVER关闭新连接接管同一个仍在运行的 Shell任意数量的?modeviewer连接可以只读地观看回放与实时输出不会获取也不会驱逐 holderShell 退出时所有连接收到含exit_code的 JSONexit帧随后 socket 关闭主动拆除会话使用DELETE /pty/:id。2.5 会话状态查询GET /pty/:sessionId返回 model/pty.go 的PTYSessionStatusResponse{ session_id: id, running: true, output_offset: 123456 }output_offset即回放缓冲的当前写入游标是重连时since的天然取值。3. 三种模式的语义与边界模式启用方式语义PTY默认无参数分配真实伪终端ANSI 与 TTY 感知工具正常工作stderr 合流进 PTY 输出Pipe?pty0无 TTYstdout/stderr 通过0x01/0x02两个独立二进制帧分开传输Viewer?modeviewer只读观察者要求会话已在运行否则会收到 409VIEWER_REQUIRES_RUNNING_SESSIONViewer 的只读约束由服务端强制viewer 若发送二进制 stdin或发送 JSONstdin、signal、resize帧会收到READ_ONLY错误帧ping仍可用。连续五次被拒绝的“变更类”帧之后服务端会直接关闭该 viewer 连接——这个上限定义在 pty_ws.go 的ptyViewerReadOnlyViolationLimit 5。4. 源码级原理Shell 启动与双模式实现4.1 Shell 选择Bash 优先sh 兜底Shell 的选择逻辑在 command.go 的shellCommand()中// shellCommand returns (shell, argv) for launching the preferred shell, // prepending --noprofile --norc when Bash is selected. func shellCommand(extra ...string) (string, []string) { shell : getShell() args : make([]string, 0, 2len(extra)) if shell bashShell { args append(args, --noprofile, --norc) } args append(args, extra...) return shell, args }即镜像里有 Bash 就用 Bash并加--noprofile --norc跳过用户 profile/rc 加载避免启动噪音干扰自动化解析极简镜像没有 Bash 时回退到sh且明确不向sh传递 Bash 专属启动参数不便携。因此需要注意在 sh 回退环境下运行的命令必须使用镜像自带sh实现所支持的语法。4.2 PTY 模式pty.StartWithSizepty_session.go 的StartPTY()通过github.com/creack/pty以初始窗口Cols: 80, Rows: 24启动 Shellptmx, perr pty.StartWithSize(cmd, pty.Winsize{Cols: 80, Rows: 24})源码中有两处值得注意的工程细节不设置Setpgid——pty.StartWithSize内部已经做了SetsidSetctty叠加Setpgid会导致EPERM会话组长不允许 setpgidstdin 就是 PTY master fds.stdin ptmx写 master 即向 Shell 喂输入PTY 模式没有独立 stderrstderrW在 PTY 模式下为 nil见ptySession结构体注释。启动后两个后台 goroutine 并行工作broadcastPTY()以 32 KiB 缓冲循环读取 master fd把每块输出同时写入回放缓冲和当前附着客户端的 pipewaitAndExit()等待 Shell 退出并关闭doneCh触发exit帧下发。4.3 Pipe 模式三路 os.Pipe 独立广播StartPipe()用三组os.Pipe()分别承载 stdin/stdout/stderrShell 以Setpgid: true启动便于整组信号投递父进程随即关闭子进程端的三个 fd。stdout 与 stderr 各由独立 goroutinebroadcastPipe广播这就是 pipe 模式下0x02stderr 帧与 PTY 模式合流输出的来源也是为什么 pipe 模式对“慢速 viewer”而言是合并的有界流、不再有独立的 stdout/stderr 回放通道见下文 4.5。5. 回放缓冲1 MiB 环形缓冲支撑since重连每次输出在扇出到 WebSocket 之前都会先写入 replay_buffer.go 的环形缓冲const replayBufferSize 1 20 // 1 MiB它是一个固定 1 MiB 容量、带单调递增字节计数total的循环缓冲区写满后最旧的字节被覆盖但total继续累加因此任意历史偏移o都能被判断“是否仍在缓冲内”o total - size单次写入大于整个缓冲时只保留最后size字节但total仍按完整长度推进维持head total % size的不变式changed通道采用“惰性创建”没有订阅者时为 nil避免无 viewer 的会话为每个输出块分配通知通道。Viewer 消费走ReadOutput(since)内部调用ReadFromAndSubscribe快照读取与订阅是原子完成的保证只读观察者不会在“快照—订阅”之间丢输出。这解释了两条文档行为输出始终为回放而缓冲重连时带since即可追赶持有者再次启动同一会话时有界回放缓冲被保留所以 viewer 用since0仍可能拿到上一代 Shell 生命周期的保留输出只要未被 1 MiB 上限覆盖。6. 连接治理409、接管与 4001pty_ws.go 的握手流程注释梳理得非常清楚先查会话404、viewer 分支直接进只读路径、非 viewer 则尝试LockWS()获取独占锁——拿不到且非?takeover1时在升级 WebSocket 之前就返回HTTP 409ALREADY_CONNECTED。接管takeover的实现在 pty_session.go 的TakeoverWS(timeout)在一个 5 秒的超时窗口wsTakeoverTimeout 5 * time.Second内循环执行“尝试 LockWS → 失败则triggerEvict()驱逐当前持有者 → 10 ms 后重试”。被驱逐的 holder 会收到应用私有范围的 WebSocket 关闭码4001WSCloseTakenOver处于 RFC 6455 §7.4.2 的 4000–4999 区间与 reasonTAKEN_OVER——这个编码刻意让客户端能区分“被有意接管”与“网络掉线”从而避免旧端自动重连后又撞上新的 holder。驱逐钩子本身也做了竞态防护每个连接通过SetEvictHandler注册带**代际 tokenevictGen**的钩子ClearEvictHandler只清除属于自己代际的钩子保证旧连接拆场时不会误删接管者的钩子钩子函数被设计为幂等对已关闭的 WS 再关闭是 no-op。7. 输出扇出与回压隔离ptySession的结构注释揭示了一个关键设计广播 goroutine 只在读取stdoutW指针时持有outMu锁实际向 pipe 写入发生在锁外避免慢客户端阻塞广播路径。对 viewer 而言隔离做得更彻底viewer不挂载扇出 pipe而是直接消费有界的回放流ReadOutput文档中“viewer 的 WebSocket 回压不会阻塞读/写 holder 的实时输出管道”正是这一实现结果的直接表述。持有者的连接断开则通过UnlockWS释放锁下一连接可重新附着。8. 错误码速查WebSocket 层的业务错误码集中在 model/pty_ws.go 的ServerFrame.Code中便于客户端程序化处理Code触发场景SESSION_GONE会话已不存在START_FAILEDShell 启动失败ALREADY_CONNECTED已有 holder 且未带takeover1升级前以 409 返回TAKEN_OVER被接管伴随 WS 关闭码 4001READ_ONLYviewer 发送了 stdin/signal/resize 帧VIEWER_REQUIRES_RUNNING_SESSIONviewer 试图附着未运行的会话409STDIN_WRITE_FAILED/INVALID_FRAME/RUNTIME_ERRORstdin 写入失败 / 非法帧 / 运行时错误REST 层的错误码NOT_SUPPORTED、CONTEXT_NOT_FOUND、RUNTIME_ERROR、INVALID_REQUEST见 pty_controller.go 的各分支例如会话不存在统一映射为404。9. 使用注意事项与边界PTTY 回显PTY 流中你的命令文本可能先于真实输出出现Shell 回显自动化断言时不要只匹配“输入行里同样出现的文本”sh 回退语法命令必须兼容镜像sh实现不能假设 Bash 特性如[[、数组1 MiB 回放上限since偏移若早于total - 1 MiB超出部分已不可回放GET /pty/:id的output_offset始终给出当前有效游标viewer 生命周期viewer 只能在 Shell 运行期间附着Shell 退出时 viewer 收到exit帧并关闭viewer 不能启动 Shell否则会先于 holder 争抢独占锁因此被 409 拒绝Windows 不支持Windows 构建下所有/pty端点返回 501相关行为以 pty_session_windows.go 的 stub 为准。10. 相关源码与测试索引关注点路径功能说明文档本文基础PTY.md路由注册/pty组 withPTY门禁中间件router.goREST 控制器创建/查询/删除pty_controller.goWebSocket 握手、帧泵、接管、viewer 只读判定pty_ws.goWS 帧模型、二进制类型字节、错误码、4001 关闭码model/pty_ws.goREST 请求/响应模型model/pty.go会话运行时启动、广播、接管锁、驱逐钩子pty_session.go1 MiB 环形回放缓冲replay_buffer.goShell 选择与--noprofile --norccommand.go协议与会话单元测试pty_ws_test.go、pty_session_test.go、replay_buffer_test.go综上execd 的 PTY 会话用“REST 管生命周期、WebSocket 管数据面、环形缓冲管可回放性、代际驱逐钩子管连接所有权”的组合实现了一个可断线恢复、可多端观察、且对慢消费者隔离的远程终端协议适合作为 AI Agent 在沙箱内执行交互式命令安装脚本、调试 Shell、长任务监控的标准通道。【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考