大家好,我是小木头。最近我们介绍了什么是 Pi Durable 并且做了 Pi Durable 的实战分享,这一期呢咱们再进一步,介绍如何把 Agent 它的执行环境从本地扩展到远程。在一些应用场景中,我并不需要 Agent 直接在我本地日常使用的工作电脑上读文件、改代码、跑命令,而是进入一个远程相对安全的环境。比如一个 GitHub 项目的开发工作,把执行环境放到一个独立的云端主机上,或许呢会是一个蛮安全的选择。
Pi Durable Harness、任务状态以及的密钥依然呢保留在本机,真正执行工具的是另一台 Linux 主机。今天分享呢我会用官方的 pi-env 这个开发包,或者这个库,我们来看看怎么配置吧。先看这张图,左边是本地控制面,如果控制面的概念有点抽象没关系,我们重点看看它包含些什么。
它包括了 Pi Durable Harness、SQLite、Task、Document,还有调用模型所需要的 provider 它的配置和密钥。右边是一台远程的 Linux 主机,里面只有工作目录和工具运行所需要的软件。中间是 SSH,以及运行在远端的 pi-env daemon。
Harness 调模型时走的是本地的 provider,模型发出 read、write、edit 或者 bash 之后,这些工具不直接触碰本机的文件,而是通过 RemoteExecutionEnv 去右边、也就是远程的主机上执行。这里有个前提:应用没有主动把 secret 注入远程的命令执行环境,架构能够确保密钥呢都留存在本机。
实验环境是一个 Ubuntu 24.04 的 Docker 容器,我用它来模拟远程环境。里面只安装 OpenSSH、Python 和基础的一些证书,再创建一个专用的 agent 用户,工作目录是 /home/agent/workspace。容器的 22 端口映射到宿主机的 2222 端口,SSH 私钥只留在项目的实验目录。
为了把这套接入过程呢完整跑一遍,我在本期的项目叫 experiment 这个目录里呢写了一个演示脚本。它运行在本地宿主机上,通过 pi-env 连接到刚才启动的这个远程环境、也就是 Docker 容器。执行脚本的 trust 子命令时,宿主机上的应用会先调用 scanHostKey() 获取到服务器的主机公钥和指纹。
当然了看到指纹还不够,我们还要从云服务器的控制台或者其他可信渠道取得正确指纹,确认两边一致。在当前这个实验里,我从容器内部读取 ED25519 主机的公钥指纹与扫描结果进行交叉确认。确认后,脚本再调用 acceptHostKey() 把这把公钥放进项目自己写的 known_hosts。
这是一项首次接入时要做的操作,并不需要每次连接都重复执行。之后 pi-env 会自动使用这份记录,并以严格的模式来检查服务器身份。如果主机公钥发生变化,连接会直接失败的,不会自动的接受新的密钥。
固定主机别名,以及禁用 agent forwarding、X11 forwarding 和端口转发等限制也会由 pi-env 自动加入 SSH 连接参数。接下来运行一个真正的 Durable Agent client,这个 client 扩展来自之前视频里的 pi-durable-mini-agent。
远程地址和端口不需要修改代码,可以通过 PI_REMOTE_HOST 和 PI_REMOTE_PORT 这两个环境变量来指定。用户是 agent,SSH 私钥和 known_hosts 都使用项目自己的文件。然后再来看 client 入口:Harness、SQLite、模型调用以及 CodingTools 都在本地,只是把 Harness 的 env 换成了指向远端工作目录的 RemoteExecutionEnv。
连接时 pi-env 先检查远端的平台,再从 npm 包里选择对应的 daemon,按照 SHA256 命名部署并在每次启动前校验。远端不需要预安装 Node.js,也不保存模型的 API key。我在对话里让 Agent 创建并运行一个 Python 文件,模型实际选择了 write 和 bash。
终端里的两条 REMOTE 日志来自于真实的工具调用。Agent 返回运行结果之后,我再进入 Docker 容器,读取同一个文件并重新运行。文件确实存在于 /home/agent/workspace 目录,输出也一致。本地项目目录里并没有这么一个工作文件。
而 Pi Durable 自带的 read、write、edit 和 bash 工具依赖的是 ExecutionEnv 这个接口,所以更换环境就能更换他们实际工作的机器。左半边为什么能恢复?要看 Task 和 Document。这里模拟的是一次远程代码验证任务,在远端运行 test_calc.py 脚本。
测试通过后记录这次操作已经完成。这个 Task 有 prepare、execute 和 verify 三个阶段。prepare 生成稳定的 operation ID,先说明一下:operation ID 和 .operations 都不是框架自动提供的,而是外部系统自行实现的操作登记和幂等机制。
这里呢用一个远端的目录来做一个最小的示例。execute 运行测试,并留下一个 receipt,它是业务代码写入的普通的文本文件,文件名带着 operation ID,内容是这次操作的结果凭证。程序再把它保存进本地的 checkpoint 和 Document。
verify 调用 readTextFile() 再次读取远端的 receipt,这里不是执行一条 ssh cat 命令。pi-env 复用 SSH 上的 daemon 会话,通过 open、pread 和 close 取回内容。远端文件和本地 checkpoint 一致,任务才结束。
画面里再用 Docker exec 加 cat,从容器内部独立地核对一番。Document 保存 stage、尝试次数和 receipt。Durable commit 把 checkpoint 和 Document 原子地写入本地 storage,恢复时继续最近一次提交的阶段。
那接下来我们模拟网络断开:第一次读取 hello.txt 成功,然后呢停止容器。后续操作返回的并不是旧数据,也不是一个模糊的超时,而是非常明确的 SSH exit code 255 以及 connection refused。
sshConnection() 这个函数是一个实现了惰性的连接,也就是说创建环境时并不需要网络,第一次操作才来建立会话。如果这次启动失败错误属于当前操作,那下一次操作仍会重新尝试。把容器再次启动回来再运行同一段读取操作呢就会成功。
最关键的实验是在最危险的位置杀掉进程:execute 阶段会先去远端运行测试,再创建 receipt 文件。第一次运行时实验脚本打印 CREATED:receipt-build-calc-v1,这里的 CREATED 只是日志提示表示这次运行新建了 receipt。文件中真正保存的是 receipt-build-calc-v1,就在远端操作已经完成本地还没来得及提交 verify checkpoint。
我让进程主动退出来模拟一次真实的突然的崩溃,现在两边看到的世界不同了,状态已经不同步了。远端已经有了 receipt,本地 SQLite 只知道 Task 还停在 execute 阶段,对本地来说这次操作的结果是 unknown outcome。
不知道远端是没做、做了一半还是全部完成。SQLite 原子事务无法跨过 SSH,把远端文件系统一起纳入同一个 commit。重启脚本后 Harness 打开同一个存储比如 durable.sqlite,找到同一个 conversation 以及 Task,再从 execute checkpoint 继续。
重新执行代码时代码携带同一个 operation ID,检查文件 .operations 下面的 build-calc-v1.done。他发现上一次留下的 receipt 文件已经存在,于是跳过远程的测试直接读回原来的结果。终端打印出的 REUSED 也只是实验脚本添加的提示,随后程序提交 verify checkpoint。
最后 Document 变成 stage done,attempts 设为一,恢复成功。这倒不是因为远端操作被自动回滚,而是因为外部操作呢使用了一个稳定的 ID,执行结果呢可以查询也可以去重。
这里先区分两类工具:第一是非 replay-safe 工具,如果进程挂在了调用中间 Pi Durable 不会擅自重跑而是给模型一个 interrupted 之类的结果并保留已经提交的部分的输出。第二是 replay-safe 工具,开发者可以在工具定义中声明这次调用和这个工具呢是允许在恢复后重新执行的。
但允许重试不代表外部操作只会发生一次,比如一笔转账已经成功返回结果时连接突然中断。Pi Durable 只知道调用没有正常返回,无法判断转账究竟有没有成功。如果直接重试就可能转成两次。发送邮件和部署也有同样的问题,外部操作最终只发生一次通常叫 exactly-once。
它不是在工具定义里增加一个字段就能够获得的能力,常见的做法是在执行外部操作之前保存一个稳定的 operation ID。工具每次重试都携带同一个 ID,远端或外部服务按这个 ID 去重并且允许查询已经产生的结果。如果目标系统不支持幂等就需要先查询状态,提供补偿的动作或者把结果标记成需要人工确认。
Pi Durable 解决的是从哪里继续,外部操作能不能够安全重试仍然由工具代码和目标系统来共同的保证。还有三点值得注意的:首先,本次分享所使用的 1.1.0 版本 Pi Durable README 依然写着 Experimental API,可能在版本之间直接发生变化。所以实验把 npm 包和源码提交都锚定了版本。
第二,这个 Ubuntu Docker 容器只是为了复现 SSH 行为,普通容器是否安全取决于运行、时挂载网络权限和主机的配置。它不是自动成立的生产级 Sandbox。第三点模型凭据留在本地是这套拓扑的默认结果,它并不是一条信息流防泄露的策略。
应用如果把环境变量和 secret 显式的传给远程命令,它依然会离开本机的环境。这是大家在使用时特别需要注意的。所以 Pi Durable 让控制面留在一个可以持久化审计和恢复的位置,再把文件与进程操作送到远程相对安全的执行环境。
对 GPU 主机、构建机、测试机,或者按会话分配的远程工作区这个机制还是蛮有用的。但只要工具会改变外部状态真正的工程工作仍然是以下 4 件事情:先提交意图,给操作确定一个稳定的 ID;让结果可以查询确认操作没有完成时再重试。
结果不确定时进行查询或者补偿,Harness 可以从 checkpoint 继续。远程操作能不能安全的继续要由应用自己的幂等或补偿机制来做回答。如果本期的内容对你有帮助,欢迎关注我们的频道也记得一键三连哦。我是小木头,我们下期见。