[Feature] 沙箱跨 Run 依赖缓存:按项目+lockfile 内容寻址复用 npm/Go/pip 等依赖目录,支持 setup/maintenance 两段式准备
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 12/100
- Issue 类型
- 功能
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- docker, go
调研方向
Start with internal/runtime/sandbox_manager.go (RunWorkspace, DestroyRun), internal/runtime/sandbox_docker.go (ensureStarted, the workspace mount) and internal/runtime/sandbox_environment.go (runEnvironmentInstall, .install-stamp) to see how per-run state is created and removed. This is cross-cutting design work touching the domain model, both sandbox drivers and the remote runner, so it is not a newcomer task. Done means the acceptance checklist in the issue is met, including the cache-isolation and concurrent-fill tests.
由索引模型根据 Issue 内容生成。
描述
背景与借鉴来源
AI 编码 Agent 每次开工都要先装依赖(npm install、go mod download、pip install……)。在大仓库上这一步往往要几分钟,而且每次下载的内容基本一样。主流平台都把“装依赖”和“依赖缓存”做成了平台能力:
| 项目 | 规模 / 活跃度 | 可借鉴做法 |
|---|---|---|
| openai/codex(Codex Cloud) | ~13.5 万 star,2026-11-11 发布 rust-v0.167.2 | 环境分为 setup script 和 maintenance script:setup 跑完后容器状态会被缓存(官方文档写最多 12 小时),之后的任务直接用缓存容器,只跑轻量的 maintenance script 刷新依赖;setup 脚本或环境变量改了,缓存就失效。用户也很在意这一点,例如 openai/codex#25086 就是在反馈缓存没生效、每次都重装 |
| GitHub Copilot coding agent(文档) | GitHub 官方产品 | 在 .github/workflows/copilot-setup-steps.yml 里声明环境准备步骤,示例直接用 actions/cache(~5.6k star)缓存依赖目录,key 取 lockfile 的 hash |
| OpenHands/OpenHands | ~9.3 万 star,每天都有提交 | 仓库根目录放 .openhands/setup.sh,每次开始工作都会执行,用来装依赖(文档) |
| e2b-dev/E2B | ~1.5 万 star,每天都有提交 | 沙箱模板把装好依赖的状态预先做成快照,从模板启动新沙箱是秒级 |
它们的共同点是:按 lockfile / 脚本内容算出缓存 key,跨任务复用依赖目录或环境快照,后续任务只做增量刷新。
LoopForge 现状与不足
(以 commit a7c6f15 为准)
- 每个 Run 一个工作区,依赖缓存全落在这个工作区里,Run 结束就跟着删掉
internal/runtime/sandbox_manager.go的RunWorkspace(runID)给每个 Run 单独分配目录root/<runID>;DestroyRun在 Run 结束后整个删掉(internal/engine/run_environment.go:321调用)。- Docker 沙箱只挂载了一个工作区(
internal/runtime/sandbox_docker.go:641:-v workspace:/workspace)。Agent 的 HOME 设成/workspace/.agentflow/private-home/<provider>(internal/runtime/command_binding.go:59),所以~/.npm、~/go/pkg/mod、~/.cache/pip这些缓存也都写在本次 Run 的工作区里。 DiscardRunContainer的注释写着 rerun 时“不需要上次填进去的 installs and caches”。也就是说,重试时依赖缓存同样被扔掉。
- 只缓存了镜像层,项目依赖没有缓存:
ensureDockerImage/cachedDockerImageName(sandbox_docker.go:766、:481)按 Dockerfile 内容的 hash 复用镜像,removeDockerSandbox也特意保留了缓存镜像。可项目自身的依赖(由 lockfile 决定)通常不会写进平台级 Dockerfile,每个 Run 都要从网络重新拉。 - 现有的“准备”机制只在单个工作区内去重
- 环境资产的
EnvironmentDefinition.InstallScript(internal/domain/assets.go:142)由runEnvironmentInstall(internal/runtime/sandbox_environment.go:154)执行。它用.install-stamp记录脚本 hash,但 stamp 放在.agentflow/environment/<id>/,也在本次 Run 的工作区里,换一个 Run 就要重装。 - 脚本节点的
RunOnWorkspaceRebuild+EnsureWorkspacePreparation(internal/runtime/workspace_preparation.go:268)用preparationFingerprint记录已执行的步骤,同样只作用于本工作区和本沙箱,无法跨 Run 复用产物。
- 环境资产的
- 已有 issue 都不涉及这件事:#218 缓存的是“纯产物节点的执行结果”,不是依赖目录;#124/#150 改进的是克隆和构建镜像的进度与超时提示;#117 讲沙箱调度解耦。
结果是:同一个项目连续跑十个 SOP,就要完整装十次依赖。既浪费时间和 Token(Agent 会等依赖、处理安装报错),又反复占用外网带宽或内部镜像源。
建议方案
做一个按项目隔离、按内容寻址的跨 Run 依赖缓存卷,配合 setup / maintenance 两段式准备脚本:
- 缓存声明(加在环境资产或沙箱模板上)
- 给
EnvironmentDefinition(或SandboxProfile)加一个Caches []CacheSpec字段,每项包含:idpaths:容器内路径,例如/workspace/.agentflow/private-home/*/.npm,或者更简单的做法:平台通过环境变量把npm_config_cache、GOMODCACHE、PIP_CACHE_DIR、CARGO_HOME等统一指向/cache/<id>keyFiles:用来算 key 的文件,例如package-lock.json、go.sum、requirements*.txtrestoreKeys:前缀回退,借鉴 actions/cache
- 内置 npm/pnpm/yarn、Go、pip/uv、Maven/Gradle、Cargo 的预设模板,用户勾选即可。
- 给
- 挂载与隔离
- Docker:创建容器时(
sandbox_docker.go的ensureStarted)对每个缓存额外挂--mount type=volume,src=lf-cache-<projectID>-<cacheID>-<keyhash>,dst=/cache/<id>。 - native/bwrap:
--bind <cacheRoot>/<project>/<id>/<keyhash>。 - 卷只在同一项目内共享,不同项目和不同用户互不可见。对 secret 敏感的目录(如
.npmrc、凭据文件)不得放进缓存,可复用gitwork/exclude.go中IsCredentialFile的判断。 - 远程 Runner 在本机维护同样结构的缓存目录,任务载荷(
RunnerSandboxSpec)里只带缓存声明,不带缓存内容。
- Docker:创建容器时(
- 两段式准备
- 现有
InstallScript作为 setup 阶段:缓存未命中时执行,执行完把 stamp 写进缓存卷而不是工作区,这样跨 Run 也认得出来。 - 新增可选的
MaintenanceScript:缓存命中时只跑它(例如npm ci --prefer-offline、go mod download)。 - setup 脚本内容、keyFiles hash、镜像 hash 中任意一项变化,缓存就失效。
- 现有
- 并发与回收
- 同一缓存 key 首次填充时加文件锁,或者先写临时卷再原子改名,避免两个 Run 同时写坏缓存。
- 后台定期按 LRU 和总容量上限清理(可放进
ReclaimCachedDockerImages所在的回收循环)。 - 项目设置页提供“清空缓存”按钮。
- 可观测
workspace.preparation.*遥测事件加上cacheHit/cacheMiss/cacheKey/restoreKey/准备耗时。- Run 详情页的“工作区准备”面板显示命中情况和节省的时间。
验收标准
- 环境资产(或沙箱模板)可以声明缓存项,并提供至少 npm、Go、pip 三种预设;API 和前端都能编辑和校验。
- 同一项目、lockfile 不变时,第二个 Run 命中缓存:只执行 maintenance 脚本(或什么都不执行),准备耗时较首次明显下降。附 Go + Node 示例仓库的前后耗时对比。
- 修改 lockfile 或 setup 脚本后 key 变化,按
restoreKeys回退到最近的缓存再做增量安装。 - 不同项目之间的缓存互不可见(有测试覆盖),凭据类文件不会写进缓存。
- 两个 Run 并发填充同一个 key 时不损坏缓存(有并发测试)。
- 缓存总量超过上限时按 LRU 清理;项目管理员可以手动清空。
- Docker 驱动和 native 驱动都支持;远程 Runner 在本机缓存,任务载荷不含缓存内容。
- 准备阶段的遥测和 Run 详情页能看到 hit/miss、key 和耗时。
- 不声明缓存时行为与现在完全一致(回归测试通过)。
- 主要语言
- Go
- 星标
- 141
- 派生
- 28
- 平均合并
- 1 天 15 小时
- 30 天内合并 PR
- 113
环境准备
- 提供 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
Tencent/LoopForge 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 74/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 75/100
维护者通常 1 天内回复
-
[Bug]: 官方镜像未安装 wecom-cli,企业微信通知报「通知服务未安装 wecom-cli」可能已有人在做 @newexploreor 于 1 天前认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 76/100
Tencent/LoopForge#156 · 1 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
【架构优化】合并 gitwork/workbranch.go 中 push/pull 重复的默认提交身份注入为 withFallbackIdentity可能已有人在做 @fx-kurumi 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 40/100
维护者通常 1 天内回复
-
【架构优化】从 runtime.installNPMCLI 中拆出 npm 参数拼装 npmCLIInstallArgs 与安装结果启用 activateNPMStage可能已有人在做 @fx-kurumi 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 30/100
维护者通常 1 天内回复
查看 Tencent/LoopForge 的全部 Issue
相似的 Issue
-
team:library-maintainers
难度 2/5 1-3 小时 新手友好度 82/100
padok-team/burrito#1208 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
bazel-contrib/rules_go#4784 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 75/100
-
难度 2/5 1-3 小时 新手友好度 62/100
POSIdev-community/aictl#40 ·
-
难度 1/5 1 小时以内 新手友好度 62/100
prime-radiant-inc/evener#6306 ·
维护者通常 1 天内回复