简体中文
从示例迁移到真实仓库
production 示例在一个一次性 Git 小仓库上跑通 了「读取 → 审批 → 修改 → 跑测试」,并且用 smoke 验证了批准、拒绝、Worker 被 SIGKILL 后的恢复、SSE 续读和取消。本文说明把它换成你自己的仓库、自己的登录系统和自己的数据 保留策略时要改哪些地方。
先读一遍 Golden Paths 里的 production 部分,确认你已经在本地跑通过 pnpm verify:production-example。
先理解示例的安全模型
示例故意把能力收得很窄,这不是为了演示方便,而是这一层设计的重点。RepositoryTools.mjs 里每个工具都把模型输入限制成 argv 或 stdin,程序本身是固定字符串:
| 机制 | 示例做法 | 为什么保留它 |
|---|---|---|
| 路径白名单 | RepoRead 只接受 src/greeting.sh 与 test/greeting.test.sh | 模型不能写出白名单外的路径,符号链接会被显式拒绝 |
| 写前校验 | RepoWrite 要求 expected_content 与磁盘内容逐字节相等 | 避免覆盖掉别人刚提交的改动,等价于一次乐观锁 |
| 测试可信 | RepoRunTests 先比对测试文件内容与预期,再执行 | 否则模型改测试就能「让测试通过」 |
| 无网络 | Docker 容器以 mode: 'none' 启动 | 仓库代码不能外联 |
迁移时最容易犯的错,是为了「让它对我的仓库也能用」而直接放宽这几条。建议先把它们映射 到你仓库的真实约束上,再放开范围。
哪些代码由 SDK 负责,哪些由你负责
production starter 会把示例模块复制进你的项目,因此先分清归属:恢复语义来自 SDK, 示例代码只负责你自己的仓库策略。迁移时不要把 SDK 已经保证的部分重写一遍。
| 关注点 | 归属 | 说明 |
|---|---|---|
| 路由队列、Session 租约、fencing token | SDK | AgentServer + AgentWorker + Runtime Store |
| 崩溃后的恢复计划(模型结果、工具结果、待审批) | SDK | DurableSessionRecoveryCoordinator,示例只在 RepositoryRecovery.mjs 里给出决策策略 |
| 工作区检查点与恢复 | SDK | DockerExecutionHost 的 checkpoint / restore / reclaim |
| 崩溃边界与恢复状态 | PostgreSQL | 启动器不缓存这类状态:路由状态、fencing token、已提交检查点都从存储读取 |
| 提交与终态事件的补齐 | 你的项目 | RepositoryState.mjs + RepositoryReconcile.mjs 是你自己的队列表与补齐策略;恢复扫描读的是 durable journal 投影,不是 transcript 投影。journal 超过单次重放预算时改看尾部:未入队的已接受请求一定是 journal 的最后一个事件,因此长会话同样能恢复 |
| 终态事件的幂等写入 | SDK | appendEvent(..., { idempotencyKey }) 让 Worker 与补齐流程并发发布同一个终态时只落一条事件 |
| durable journal 投影 | SDK | projectDurableSession 把已接受的请求从 journal 还原出来;transcript 投影可能根本没有这条输入 |
| 工具白名单与审批策略 | 你的项目 | RepositoryTools.mjs 与 RepositoryState.mjs 里的审批记录 |
| smoke 的断言 | 你的项目 | smoke.mjs 断言的是示例 fixture,换成你的仓库时要重写 |
RepositoryDemoProvider.mjs 只服务于 smoke 的确定性模型输出,接入真实模型后可以删除。
第 1 步:换成你的仓库
示例在启动时把一个 fixture 复制成临时 Git 仓库。换成真实仓库时改 run.mjs 里创建 repositoryPath 的那段:
js
// 原示例:复制一次性 fixture
const repositoryPath = join(temporaryRoot, 'repository');
await cp(join(root, 'fixture'), repositoryPath, { recursive: true });
await execFileAsync('git', ['init', '--quiet', repositoryPath]);
// 换成真实仓库:克隆一份,不要直接在工作副本上跑 Agent
const repositoryPath = join(temporaryRoot, 'repository');
await execFileAsync('git', ['clone', '--quiet', '--depth', '1',
process.env.AGENT_REPOSITORY_URL, repositoryPath]);要点:
- 永远让 Agent 工作在克隆或 worktree 上,不要指向开发者的工作副本。示例的 checkpoint 恢复会替换文件内容,直接作用于工作副本会丢改动。
- 私有仓库用部署密钥或短期凭据;这套凭据只用于
git clone,不进入容器环境变量。 - 想固定到某次提交,clone 之后
git -C <dir> checkout <sha>,并把该 SHA 记进日志, 便于事后追溯这次 Agent 到底改了什么。
第 2 步:改工具白名单
RepositoryTools.mjs 里的三个固定程序都要按你的仓库改:
READ_FILE的case分支:列出允许读取的路径。读整个仓库通常不现实,先挑 Agent 真正需要的文件,例如构建脚本、目标源文件、配置。WRITE_FILE的第一个判断:test "$1" = src/greeting.sh换成允许写入的路径集合。 只允许写源文件,测试文件与 CI 配置保持在白名单外。RUN_TESTS:把sh test/greeting.test.sh换成你仓库真实的测试命令,并保留它前面的 测试文件内容比对。
如果白名单变长,建议改成从仓库根的一份配置读入,而不是散落在 shell 字符串里:
js
const allowedWrites = new Set(['packages/api/src/handler.ts', 'packages/api/src/schema.ts']);
const allowedReads = new Set([...allowedWrites, 'package.json', 'pnpm-lock.yaml']);同时把工具描述改成你的路径,否则模型会按示例里的 src/greeting.sh 去猜:
js
defineTool({
name: 'RepoWrite',
description: 'Replace an allowed source file after approval.',
// ...
});第 3 步:接自己的登录系统
示例的认证只有一个共享 token:
js
authenticate(request) {
if (request.headers.get('authorization') !== 'Bearer local-demo') return null;
return { tenantId, subject: 'browser-user', scopes: ['session:admin'] };
}authenticate 返回的 principal 决定三件事:tenantId 隔离存储与事件、subject 参与 审批隔离、scopes 决定命令授权。换成真实登录时保持这个形状:
js
authenticate(request) {
const claims = await verifySessionCookie(request.headers.get('cookie'));
if (!claims) return null;
return {
tenantId: claims.organizationId,
subject: claims.userId,
scopes: scopesForRole(claims.role),
};
}需要知道的边界:
- 不要把
session:admin发给普通用户。 它满足全部 scope。按角色拆成session:create/session:read/session:write/permission:resolve(session.fork需要session:read+session:create)。 - 审批按 tenant、Session、
subject、permissionRequestId四元组隔离。 同一个 Session 里换个人点「Approve」不会命中,这是刻意的。 - 跨租户访问统一返回
SESSION_NOT_FOUND,不会泄露 Session 是否存在,别把它当成 「找不到」来排查。
第 4 步:定数据保留策略
示例在退出时删掉临时数据库和 checkpoint。生产环境要反过来,明确保留什么、留多久:
| 数据 | 示例行为 | 生产建议 |
|---|---|---|
| 路由、事件、审批、transcript | PostgreSQL,随容器删除 | 独立实例 + 定期备份;事件表按时间分区 |
| 工作区 checkpoint | 本地 checkpointDirectory | 换成共享存储或受控对象存储,否则跨机器恢复会失败 |
| Agent 的 Git 改动 | 留在临时仓库里 | 让 Agent 推分支或产出 patch,人工评审后再合并 |
| Docker 工作区卷 | 随容器删除 | 不要长期保留,重建成本低且易残留敏感内容 |
PostgreSQL 侧的 schema 版本是 3,initialize() 会在全局 advisory lock 下迁移;升级 SDK 版本时先读 Runtime Store 的 schema 小节。
注意本地 checkpoint 只能用在同一台机器上恢复。多副本部署必须实现共享 ExecutionHost, 或用受控对象存储上传 checkpoint——把本机 checkpoint ID 当分布式事实源会在换机器恢复时 直接失败。
第 5 步:从 smoke 到真实使用
smoke.mjs 现在校验的是 fixture 的固定输出(Tests passed (exit 0).)。换成真实仓库后 它必然失败,这是好事:它提醒你验收标准变了。建议:
- 保留 smoke 的流程断言(审批发生、Worker 被杀后能恢复、SSE 能续读、取消有效), 把内容断言换成你仓库的判定,例如「指定测试文件从红变绿」。
- 先用
--smoke走确定性 provider,确认链路;再接OPENAI_API_KEY换成真实模型。 - 上线前明确一件事:示例使用单个 API 进程,不提供 API 故障切换,也不保证任意工具 恰好执行一次。测试中断且结果未知时必须停下来对账,不要自动重试。