computer 项目中的容器启动规格(ContainerLaunchSpec):launched / adopted / relaunched 三态语义解析
2026/9/16 18:41:36 网站建设 项目流程

computer 项目中的容器启动规格(ContainerLaunchSpec):launched / adopted / relaunched 三态语义解析

【免费下载链接】computerGive your agent a computer 👾项目地址: https://gitcode.com/GitHub_Trending/computer1/computer

导读

本篇文章聚焦@cloudflare/computer("Give your agent a computer 👾")容器后端的一次重要接口演进:IWorkspaceContainerAPI.start()restart()从"两个松散参数"收敛为单一ContainerLaunchSpec对象,并为每次启动留下可持久化的启动记录,最终返回launchedadoptedrelaunched三种明确结果。读完本文,你将掌握该启动规格的数据结构、启动记录的生成与比对原理、容器采纳(adopt)与重启(relaunch)的判定规则,以及预热池(warm pool)这类预启动场景如何借助setInactivityTimeout()不再绕过接口直接触碰ctx.container

该变更记录于仓库的 .changeset/container-launch-spec.md,属于@cloudflare/computer包的 minor 版本更新;其完整实现与测试位于 packages/computer/src/backends/container/ 目录。

一、变更概览:从"双参数"到"单一规格"

在旧接口中,启动容器需要分别传递两个独立的参数(环境变量集合与互联网开关)。新接口将二者合并为一个结构化对象,签名统一为:

start(spec: ContainerLaunchSpec): Promise<ContainerRuntimeInfo>; restart(spec: ContainerLaunchSpec): Promise<ContainerRuntimeInfo>;

变更点可以归纳为四条:

  1. 单一参数start()restart()只接收一个ContainerLaunchSpec,不再接收两个参数;
  2. 三态返回值:调用结果明确区分为launched(新启动)、adopted(采纳已运行且规格一致的容器)、relaunched(替换掉规格不匹配的旧容器);
  3. 启动留痕:每次启动都会把规格记录到 Durable Object 存储中,供后续采纳判定使用;
  4. 接口补齐setInactivityTimeout()正式加入IWorkspaceContainerAPI接口,预热池等预启动容器的调用者无需再绕过接口去访问ctx.container

二、ContainerLaunchSpec 数据结构

ContainerLaunchSpec定义在 packages/computer/src/backends/container/container-launch-record.ts:

export interface ContainerLaunchSpec { // Environment for the container image. The launch adds // RPC_CLIENT_SECRET on top, so no caller needs to know it exists and // it stays out of the digest below. env: Record<string, string>; // Platform switch for outbound internet. Cannot be changed on a live // container, which is why a mismatch has to relaunch. enableInternet: boolean; }

两个字段各有关键语义:

  • env:传给容器镜像的环境变量。注意启动流程会在其上自动追加RPC_CLIENT_SECRET(见下文"客户端密钥注入"),调用方无需感知它的存在,且该密钥不会进入规格摘要,避免不必要的敏感信息扩散。
  • enableInternet:出站互联网的平台级开关。它只能在容器进程启动时设定,运行中的容器无法修改——这是整个"规格不匹配就必须重启"设计的根源。

三、三态结果:launched / adopted / relaunched

ContainerRuntimeInfo(定义于 container-host.ts)携带runtimeIdclientSecretoutcome三个字段,其中outcome就是本次调用的实际动作:

outcome含义触发条件
launched本次调用真实启动了新容器上一代容器已退出;或当前没有运行中的容器;或执行了restart()
adopted复用了已运行的容器容器已在运行,且其启动记录与本次请求的规格完全一致
relaunched替换了旧容器并重新启动容器已在运行,但启动记录缺失或与本次规格不一致

返回runtimeId是"运行中容器进程的持久标识"——即使 Durable Object 实例被重建,只要容器进程仍存活,同一容器会沿用同一runtimeId(见 container-host.ts 中对CurrentContainerRuntimeIdentity的复用逻辑)。

四、启动记录:规格的持久化摘要

为什么需要"启动记录"?因为在容器已运行的情况下,Durable Object 无法事后得知它当初是用什么环境、什么互联网开关启动的。而采纳(adopt)一个规格不一致的容器意味着悄悄丢弃调用方想要的环境与网络策略。于是每次启动都写一条记录,采纳时做比对。

记录结构同样定义在 container-launch-record.ts:

export interface ContainerLaunchRecord { enableInternet: boolean; // A digest rather than the environment itself: containerEnv is // consumer-supplied and may carry their own secrets, and this record // only ever needs to answer "the same or not". envDigest: string; }

关键设计是记录摘要而非环境明文

  1. 防泄密env由调用方提供,可能携带调用方自己的密钥(如API_TOKEN),落盘只存摘要;
  2. 只回答一个布尔问题:记录的唯一用途是判断"相同或不同"(sameLaunch),摘要足以胜任。

摘要算法(digestEnv,见 container-launch-record.ts)有两处工程细节:

  • 键排序:先对键排序,再以长度:键=长度:值形式拼接,保证两个以不同书写顺序构造的相同环境得到相同的规范字符串(测试 container-launch-record.test.ts 验证了这一点);
  • 长度前缀:每个键和值都带长度前缀,避免"键值拼接"出现歧义,杜绝不同组合被重排后得到相同输入;
  • 最终以 SHA-256 计算摘要,输出 64 位十六进制字符串。

比对函数sameLaunch同时比较互联网开关与摘要:

export function sameLaunch(a: ContainerLaunchRecord, b: ContainerLaunchRecord): boolean { return a.enableInternet === b.enableInternet && a.envDigest === b.envDigest; }

测试覆盖了"值变化""变量新增""互联网开关切换"三类不匹配场景,以及"摘要中不出现明文密钥"(见 container-launch-record.test.ts)。

五、start() 的分支逻辑:为何不匹配必须 relaunch

WorkspaceContainerAPI.start()的完整判定流程见 container-host.ts,核心顺序为:

  1. 读取上一代退出信息:若容器曾异常退出(priorExit !== null),先尽力destroy清理平台侧残留,然后无条件以launched启动新一代;
  2. 容器未运行:直接以launched启动;
  3. 容器正在运行:读取启动记录:
    • 记录存在且sameLaunch(actual, requested)为真 →adopted,复用runtimeId
    • 记录为null(容器由接口之外的代码启动)或记录不匹配 → 记录告警日志,destroy旧容器后以relaunched重启。

第二步的告警日志区分了两种原因(container-host.ts):

  • actual === null:"container was started outside WorkspaceContainerAPI; relaunching so the requested environment applies"(容器由接口外部启动,重启以应用请求的环境);
  • 记录不匹配:"running container was launched with a different spec; relaunching"(运行中的容器使用了不同规格,正在重启)。

对外部启动容器的处理是变更中特别强调的安全语义:一个没有启动记录的容器,宁可重启也不信任——因为它无法证明环境与网络策略符合调用方要求。

六、restart() 与 setInactivityTimeout()

restart():无条件的新一代

restart()(container-host.ts)比start()更简单直接:先destroy当前容器(尽力而为,容忍平台侧抖动),再无条件以launched启动新一代。它被用于两类场景:

  • 启动就绪失败:容器端口迟迟未打开、健康探测无法通过;
  • 租期健康检查判死:当前容器代已被判定为死亡。

值得注意的是,restart()自身不做重试循环,重试次数由调用方(即CloudflareContainerBackend)控制。

setInactivityTimeout():接口化的闲置超时

setInactivityTimeout(durationMs: number): Promise<void>;

该方法(container-host.ts)直接透传平台容器 API。它被显式加入IWorkspaceContainerAPI接口,动机在源码注释中写得很清楚:预热池等"预启动容器"的调用者,无需再绕过接口去访问ctx.container

预热池的典型做法是先启动容器再设置闲置超时(容器空闲到指定毫秒数后由平台回收),例如 examples/think-compare-runtimes/worker/computer-container-pool.ts 中的startWarmContainer

async startWarmContainer(spec: ContainerLaunchSpec, inactivityTimeoutMs: number): Promise<void> { // Through the workspace API rather than ctx.container, so the launch // carries whatever the API adds — today the shared secret the // daemon's HTTP surface requires — and is recorded, so the workspace // that adopts this container can tell it matches. await startWorkspaceContainerAndWait(this.getWorkspaceContainer(), spec, inactivityTimeoutMs); }

预热池通过getWorkspaceContainer()获得WorkspaceContainerAPI再调用启动,既让启动记录得以写入(后续 Workspace 采纳时可判定匹配),也保持了接口的完整封装。

七、调用方视角:CloudflareContainerBackend 如何消费新签名

CloudflareContainerBackendWorkspaceContainerAPI的主要驱动者(cloudflare-container.ts),它构造规格的方式展示了新签名的实际用法:

const env = { PORT: String(this.#options.containerPort), MOUNT_POINT: "/workspace", ...this.#options.containerEnv, }; let runtimeId: string; let clientSecret: string; try { ({ runtimeId, clientSecret } = await host.start({ env, enableInternet: this.#egress.mode === "direct", })); } catch (error) { // ...包装为 WorkspaceTransportError }

要点:

  • env由后端默认值(PORTMOUNT_POINT)与调用方提供的containerEnv合并而成,调用方值优先;
  • enableInternet直接来自 egress 策略:mode === "direct"时才开启;
  • 解构返回值中的runtimeIdclientSecretoutcome在后端内部用于诊断日志。

在就绪重试路径(#readyWithRestarts,cloudflare-container.ts)中,重启同样携带完整规格:

({ runtimeId } = await host.restart({ env, enableInternet: this.#egress.mode === "direct", }));

八、预热池的采纳场景:为什么规格必须一致

预热池(warm pool)是本变更最直接的使用场景之一。池子会预先启动一批容器等待 Workspace 采纳,而采纳方可能要求不同的环境、甚至完全关闭互联网。若直接复用规格不一致的预热容器,采纳方的安全与运行要求就会被静默违背。

examples/think-compare-runtimes/worker/computer-container-pool.ts 中workspaceLaunchSpec的注释点明了这一权衡:

function workspaceLaunchSpec(env: WorkspacePoolEnv): ContainerLaunchSpec { return { env: { PORT: String(WORKSPACE_PORT), MOUNT_POINT: "/workspace", ...(env.FUSE_MOUNT ? { FUSE_MOUNT: env.FUSE_MOUNT } : {}), }, // A pool cannot know the egress policy of the workspace that will // adopt a container, so this has to agree with it by configuration. // Disagreeing costs a relaunch on adoption, not the policy: the // adopting workspace compares this spec against its own and // replaces the container rather than inheriting the wrong one. enableInternet: true, }; }

池子无法预知将来哪个 Workspace 会采纳容器,因此出站策略只能通过配置对齐;一旦不一致,代价是采纳时的一次 relaunch,而绝不会让采纳方继承错误的网络策略——这正是"规格记录 + 比对"机制的价值所在。

九、客户端密钥注入:规格之外的一次自动合并

#launchAs内部(container-host.ts),实际传递给平台容器 API 的环境变量会在用户规格之上追加共享密钥:

this.#container.start({ enableInternet: spec.enableInternet, env: { ...spec.env, RPC_CLIENT_SECRET: clientSecret }, });

clientSecret通过ContainerClientSecret.ensure()任何启动之前解析,保证写入容器环境的值与后续化身(incarnation)读回并展示的值一致。而该密钥不参与摘要——调用方无需知晓其存在,也不会因它的存在导致规格比对失真。此外启动记录在启动被平台接受之后才写入,失败的启动不会留下"容器持有该规格"的虚假记录。

该密钥还承担鉴权职责:容器 HTTP 面要求 Bearer 认证(bearerMatches采用逐字节比较、不提前退出的恒定时间风格实现,见 cloudflare-container.ts),#requireAuthEnforced还会在握手前确认容器确实在强制校验(cloudflare-container.ts)。

十、测试验证:规格比对的正确性边界

container-launch-record.test.ts 为本文涉及的机制提供了可复现的验证矩阵:

  • 键序无关性{ A: "1", B: "2" }{ B: "2", A: "1" }产生相同摘要;
  • 值变化检测FUSE_MOUNTauto变为none即判定不匹配;
  • 变量新增检测:新增一个EXTRA变量即判定不匹配;
  • 互联网开关区分enableInternettrue/false判定为不匹配(对应 egress 场景);
  • 不落明文:含API_TOKEN: "hunter2"的规格,序列化记录中不出现明文,envDigest匹配^[0-9a-f]{64}$
  • 记录读写往返CurrentContainerLaunchRecord能正确 round-trip;
  • 外部启动场景:仅有 runtime identity、无启动记录时get()返回null,从而触发 relaunch 而非信任。

容器主机的采纳/重启行为另有 container-host-adoption.test.ts 与 cloudflare-container.test.ts 覆盖。

十一、迁移与使用要点

对于使用@cloudflare/computer/backends/container的开发者,本次 minor 变更的影响与建议如下:

  1. 签名迁移:所有host.start(env, enableInternet)形式的调用改为host.start({ env, enableInternet })restart()同理;
  2. 消费三态结果ContainerRuntimeInfo.outcome可用于观测与告警——高频出现relaunched通常意味着预热池与采纳方的规格配置存在分歧,值得排查;
  3. 预热池对齐规格:如 computer-container-pool.ts 所示,预热规格中的envenableInternet应与可能采纳它的 Workspace 保持一致,避免采纳时频繁 relaunch;
  4. 走接口而非绕过:预启动容器时请使用getWorkspaceContainer()返回的WorkspaceContainerAPI并调用setInactivityTimeout(),这样启动会留下记录,且后续采纳方能正确判定匹配;
  5. 接口导出ContainerLaunchSpecIWorkspaceContainerAPIWorkspaceContainerAPIContainerRuntimeInfoWorkspaceRefwithWorkspaceContainer均从 packages/computer/src/backends/container/index.ts 导出,可通过import { CloudflareContainerBackend, withWorkspaceContainer } from "@cloudflare/computer/backends/container"引入。

结语

ContainerLaunchSpec的引入并非一次简单的参数收敛:它以"规格记录 + 摘要比对"为支点,把"容器已运行时环境与网络策略不可变"的平台约束,转化成了清晰的三态结果与可观测的 relaunch 语义。对于容器化 Agent 运行时中常见的预热池、跨 DO 采纳、egress 策略切换等场景,这一设计让"复用"与"安全"的边界变得明确——复用必须建立在规格可证明一致的前提之上,否则宁可重启,也不静默违背调用方的意图。

【免费下载链接】computerGive your agent a computer 👾项目地址: https://gitcode.com/GitHub_Trending/computer1/computer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询