这周我们发了两个版本,让 Appaloft 从一个部署控制面变成我们现在叫做 AI 应用交付平台的东西。v1.1.0 加入了 execution sandbox 平台和带 promotion 工作流的 sandbox agent runtime,v1.2.0 补上了 SDK 资源句柄,应用侧不用再手写 API 调用。
目标用户不是部署个人项目的人,而是正在做“让 coding agent 干活”这类产品的开发者:chat-to-app 功能、仓库维护机器人、会改代码的客服自动化。agent 负责构建,Appaloft 负责让构建出来的东西可以被检查、被冻结、被有意识地发布。
整个功能里最重要的设计决定也最简单:产出成果的 agent,永远不是按下发布按钮的那个角色。
活的工作区不是可部署的东西
给 agent 一个 shell,它会很乐意给你跑起一个 dev server。这种 demo 很好做。难的是之后回答一个问题:你要发布的确切是什么东西。
一个活着的沙箱工作区,在你看它的那一刻和你部署它的那一刻之间,是可以变化的。快照可能把不该进入部署输入的运行时状态一起打进去。而一个既能写代码又能按发布键的 agent,离“把没人审过的东西发上线”只差一次 prompt injection。
所以我们把这条链路拆成几个各有归属的对象,并让每一次状态转移都显式发生。
谁拥有什么
Appaloft 里的 Sandbox 是一个受控执行环境,不是递给 agent 的 VPS 账号。它有资源限额、默认拒绝的网络策略、文件和进程 API、模板来源、过期时间,以及精确的清理行为。跑 sandbox provider 的主机不会暴露给应用开发者,也不会暴露给 agent。
一个 Agent Runtime 从属于且只从属于一个 Sandbox,并随它一起销毁。一个 Runtime 同一时间只允许一个活跃的 Run,每个 Run 是一次提交的任务,上下文血缘显式标记为 fresh 或 continue(parentRunId)。聊天和会话状态留在你的应用里,Appaloft 不存任何隐藏的模型推理过程。Run 事件有数量、深度和长度上限,credential、secret、password、token、authorization 这些字段会被递归脱敏。它是运维回读,不是审计日志,也不是完整 transcript。
const sandbox = await appaloft.sandboxes.create({
source: { kind: "template", templateId: process.env.APPALOFT_SANDBOX_TEMPLATE_ID! },
requestedIsolation: "gvisor",
limits: { cpuMillis: 2_000, memoryBytes: 2_147_483_648, diskBytes: 10_737_418_240, maxProcesses: 128 },
networkPolicy: { mode: "deny", rules: [] },
expiresAt: new Date(Date.now() + 60 * 60 * 1_000).toISOString(),
});
const agent = await sandbox.agents.create({ harness: "pi" });
const run = await agent.runs.create({ task: "Build the requested app in /workspace/app" });
Runtime 在设计上与 harness 无关。Pi、Codex、Claude Code 各有各的会话、事件和审批模型,任何一家的模型都不该变成 Appaloft 的公共语言。harness 实现 AgentHarnessPort,Pi 是第一个 adapter,用不可变的 harness 模板和 skill bundle digest 固定版本,而不是一个公开 API 族。以后换 harness,Sandbox → Runtime → Run 这条归属链不会变。
取消语义也做了刻意处理:harness 以后台可终止进程的方式运行,取消一个 Run 会杀掉这个进程,并阻止迟到的成功结果覆盖 cancelled 状态。
秘密不进沙箱
生产凭据不允许出现在沙箱的环境变量、文件、Run 事件、报错或快照里。当一次运行确实需要访问外部系统时,访问走目标绑定的凭据 broker,只要目的地、方法、过期时间或转换规则有一项对不上,就直接失败关闭。
权限同理。封闭在沙箱策略内的工作——文件、进程、安装、测试、构建、dev server——可以自由执行。但网络放大、凭据授予、公网端口暴露、对外写入和 promotion,都需要控制面的持久化审批。harness 不能批准自己。审批请求会带上能力、目的地、请求摘要和过期时间,方便你的应用把“到底在请求什么”原样展示给人看。
先冻结,预览冻结后的东西,再 promote
从工作区到生产是三步,每一步都有自己的证据点。
第一步,捕获。沙箱空闲且没有活跃 Run 时,Appaloft 校验 source root,对每个文件做哈希,产出不可变、按内容寻址的 Source Artifact,带 digest、有序 manifest 和来源信息。捕获会拒绝秘密、设备与 socket 条目、不安全的链接,以及 source root 之外的一切路径。
第二步,预览。Candidate Preview 从确切的 artifact digest 物化出来,只读 artifact store,永远不读可变的活工作区。dev server 的 URL 不能当审批证据,对冻结字节的精确预览才可以。
第三步,promote。Promotion 是一对 plan/accept:
const artifact = await appaloft.sandboxes.sourceArtifacts.create({ sandboxId, sourceRoot: "app" });
const preview = await appaloft.sandboxes.candidatePreviews.create({ artifactId: artifact.data.artifactId });
const plan = await appaloft.sandboxes.promotions.plan({
sandboxId,
artifactId: artifact.data.artifactId,
expectedArtifactDigest: artifact.data.digest,
candidatePreviewId: preview.data.previewId,
target: { projectId, environmentId, destinationId, resourceName: "Generated app" },
});
if (preview.data.artifactDigest !== artifact.data.digest) throw new Error("digest mismatch");
plan 绑定 artifact digest、已验证的候选预览、目标和过期时间。accept 要求 plan 未过期、digest 匹配,并且由沙箱外拥有发布权限的角色发起。Runtime 和 harness 身份都不能 accept。真实产品里,你应该在 plan 之后停下来,把候选 URL 和确切 digest 展示给用户,只有用户在沙箱外做了显式动作才调 accept。
accept 是幂等且异步的。第一次 promotion 会创建一个以 zip-artifact 为 source binding 的 Resource 和它的第一个 Deployment;重试会用同一个 artifact 开一个新的部署尝试,而不是再建一个 Resource。部分结果会被保留,进程重启不会重复创建已经记录过的东西。
proof 是什么,以及它不是什么
只有当关联的 deployment proof 判定为 verified 时,promotion 才算 completed。这个判定把 artifact digest 与来源、promotion plan 与审批、部署身份与回读,以及路由、健康、内容相关性这类可机器验证的观测串成一条链。
这里我们想把话说得小心一点。这条链证明的是 Appaloft 能观测到的交付事实,它不证明 agent 生成的软件是正确的、安全的、没有漏洞的或者合规的。测试、评审、扫描和人工审批仍然是各自独立的门禁。“Ship with proof”的意思是你可以查证发了什么、它从哪来,而不是模型做对了。
诚实的成熟度标注
以上内容在文档里都标注为 private preview,这个标注有具体含义:
- Pi adapter 要求运维侧先准备一个按版本和 digest 固定的沙箱模板,并配置
APPALOFT_PI_SANDBOX_TEMPLATE_ID。 - 候选预览目前只覆盖可静态发布的产物。
- Agent 操作目前需要 product session,还没有可用的长期应用级凭据。
- 托管 worker、gVisor、内部网络和网关的可用性取决于你的安装方式。
链路的另一半 Deploy & Verify——目录、Git、zip、镜像、Docker、Compose、静态 artifact 部署,加上健康检查、日志、重试、回滚和 proof 回读——是 available 状态,没有变化。沙箱这条链路是接在它上面的,不是替换它。
如果你正在做“agent 生产软件”的产品,可以直接跑的入口是 Chat-to-App、人工审批 和 Preview-to-Promotion 三个示例,配套文档是 sandbox、预览与 promote 和交付证据链。
我们最想要反馈的是审批这个面:在让用户 accept 一次 promotion 之前,你的产品需要把哪些信息展示给用户看?