跳到主要内容

第 8 章:执行环境、仓库、本地目录与 GC

8.1 为什么每个 Task 需要自己的环境

Provider CLI 会写很多隐式状态:

  • 工作树和 Git 分支;
  • CLAUDE.mdAGENTS.md、Skills 与 MCP 配置;
  • Provider Home、Session、Memory、Settings;
  • 日志、输出、临时文件;
  • 任务令牌和环境变量。

如果所有 Task 共用用户 HOME 和同一 Checkout,最先出现的不是性能问题,而是上下文串线、凭据泄漏和并发覆盖。

execenv 的职责,就是把每次执行所需的“文件系统世界”显式化。

8.2 标准目录布局

PredictRootDir 计算:

{workspacesRoot}/
└── {workspaceID}/
└── {shortTaskID}/ # Env Root
├── workdir/ # Agent 初始 cwd
├── output/ # 结构化输出/辅助产物
├── logs/ # Task 日志
├── codex-home/ # Provider 专用,按需
├── task-home/ # 隔离 HOME,按需
├── ... provider overlays
├── .multica-sidecars.json # 注入文件清单
├── .managed-env-provenance # 可恢复环境来源
└── .gc_meta.json # 完成后最后写入

实际子目录随 Provider 变化。关键约束是:Env Root 由 Daemon 管理,而 Agent Workdir 可能位于其中,也可能被 local_directory 替换为用户目录。

8.3 Prepare 的主要步骤

execenv.Prepare 大致做:

  1. 校验 Workspace Root、Workspace ID、Task ID;
  2. 恢复 Root Marker,确保任务内 CLI 可 Fail Closed;
  3. 防御性清理同 Task ID 的残留 Env;
  4. 创建 workdir/output/logs
  5. 写 Task Context 与 Provider 原生 Brief;
  6. 写 Project Resources 和 Skills;
  7. 记录 Sidecar Manifest;
  8. 对标准 Issue 环境写 Managed Provenance;
  9. 准备 Codex/Claude/OpenClaw/Cursor/Hermes 等 Provider 专用 Home/Config;
  10. 返回统一 Environment

Daemon 在 Prepare 前就把预测 Env Root 标记为 Active,防止 GC 与创建目录竞态。

8.4 根 Marker 与任务内 CLI 的 Fail Closed

Agent 进程使用 multica CLI 操作 Issue、Comment、Repo、Attachment、Squad 等业务。它通常从环境变量拿 Task Token 和 Task ID。

如果某个子进程丢失这些变量,却仍能读取用户的普通 CLI 配置,就可能退化成 Daemon Owner 的长期 PAT,扩大权限。

Workspaces Root 与 Workdir Marker 告诉 CLI:

你处于 Daemon 管理的任务环境。如果任务凭据缺失,不允许静默回落到普通用户配置。

Marker 在 Prepare 和 Reuse 时都会自愈,避免运行中被删除后下一次恢复失去保护。

8.5 Task HOME 与 Provider HOME

Multica 不只改变 cwd,还为不同 Provider 隔离其发现路径:

  • 通用 Task HOME:减少 CLI 向真实 HOME 写入;
  • Codex:每任务 CODEX_HOME,受控复制配置、Skills、Session Rollout;
  • Claude:任务级 Settings Path 与项目 .claude 内容;
  • Cursor:独立 Data/MCP 配置;
  • OpenClaw:临时 Config/Gateway Pin;
  • Hermes:派生 HERMES_HOME 与 Skills Overlay;
  • 其他 Provider:使用其原生 AGENTS.md、Skill Root 或参数。

“隔离”不等于完全复制真实 HOME。某些 Provider 的登录凭据必须可见,系统用白名单、Symlink 或特定文件播种,尽量只暴露运行所需部分。

8.6 Codex 的特殊隔离

Codex 对配置、Session Rollout、Memory、Skill 与 Sandbox 有较强 HOME 语义,所以相关实现拆成多个文件:

几个关键点:

  • Session Store 按 Profile/Agent/Issue 建稳定命名空间;
  • 只为要 Resume 的 Session 暴露对应 Rollout,避免看见机器上其他对话;
  • CodexResumeRolloutPresent 在启动前验证恢复资料真的存在;
  • Shell Environment 使用 Allowlist,显式授权变量优先;
  • Linux Workspace-write 对外部 Git Metadata 的限制,促成 Isolated Checkout 模式。

这是一个典型的 Provider-native 隔离层:统一接口不代表所有 Provider 用同一文件布局。

8.7 仓库为什么先做 Bare Cache

Workspace Repo 由 repocache.Cache 管理:

{workspacesRoot}/.repos/{workspaceID}/{host+org+repo.git} # bare clone

├── task A worktree
├── task B worktree
└── task C isolated checkout

优点:

  • 远端对象只下载一次;
  • 每 Task 有独立分支和 Worktree;
  • 后续 Fetch 快;
  • 不同 Repo 可并行;
  • 同一 Bare Repo 的变更操作用专用 Mutex 串行,避免 Git Lock 冲突。

Repo 目录名包含 Host、Namespace、Repo,并对端口做无碰撞编码,避免不同远端静默落到同一个缓存目录。

8.8 为什么 Workdir 一开始是空的

Prepare 明确创建空 Workdir,不自动 Checkout 所有 Workspace Repo。Agent 根据 Brief 使用:

multica repo list
multica repo checkout <url> [--ref ...]

按需 Checkout 的好处:

  • 多 Repo Workspace 不必每次展开全部仓库;
  • Agent 先读任务再选择目标;
  • Review Task 可指定精确 Ref;
  • Checkout 权限和允许列表由任务内 CLI 校验;
  • Repo 操作统一记录在 Daemon Cache,而不是让 Provider 自己猜目录。

8.9 Worktree 创建细节

CreateWorktree 会:

  1. 在 Workspace Cache 中查 Bare Repo;
  2. 持有该 Repo 的 Mutex;
  3. Fetch 最新远端,失败则警告并可使用缓存快照;
  4. 解析请求 Ref 或远端默认分支;
  5. 创建 agent/{sanitized-agent-name}/{short-task-id} 分支;
  6. 在 Task Workdir 下创建 Repo 目录;
  7. 若是复用环境,更新现有 Worktree;
  8. 排除 .agent_contextCLAUDE.mdAGENTS.md、Provider Skill 目录等注入文件;
  9. 按 Workspace 设置安装/移除 Co-authored-by Hook。

Fetch 使用 remote-tracking Refs,而不是让远端 Head 覆盖 refs/heads/*,避免与 Agent Worktree 锁定的分支冲突。

8.10 Isolated Git Metadata

普通 git worktree.git 指向 Bare Cache 中的外部 Admin Dir。某些 Sandbox(尤其 Linux Codex Workspace-write)即使把 Worktree 标为可写,也可能把解析后的外部 Gitdir 视为只读。

IsolatedGitMetadata 模式创建本地 .git 元数据,仍从 Cache 加速对象,但把需要写的 Git 控制文件放进 Workdir。它牺牲一点空间,换取 Sandbox 内可预测的 Git 写入。

一旦某个复用目录已经迁移为 Isolated Checkout,后续即使旧客户端不再传 Hint,也继续沿用安全形状。

8.11 local_directory:为什么它不是普通 Resource

用户可以把 Project 绑定到某台 Daemon 的已有目录:

{
"resource_type": "local_directory",
"resource_ref": {
"local_path": "/Users/alice/src/project",
"daemon_id": "machine-7"
}
}

此时 Agent 直接在用户目录工作,不创建 Git Worktree。适合:

  • 已有未提交修改;
  • 私有或无法 Clone 的目录;
  • 大型 Monorepo;
  • 本地工具链强依赖绝对路径。

代价是风险显著上升:它会触碰用户真实文件,因此需要比普通 Worktree 更强的验证、串行和清理。

8.12 Local Directory 的选择规则

localDirectoryAssignmentForTask 只选择:

  • resource_type=local_directory
  • daemon_id 与当前 Daemon 相同;
  • 当前 Project 对该 Daemon 恰好一条匹配记录。

若两条同时匹配,Daemon Fail Fast,不“随便取第一条”。

Squad Leader Task 特意不绑定本地目录,因为 Leader 是协调者,它可能只创建子 Issue/Comment。让 Leader 长时间持有 Repo 锁会阻塞真正写代码的 Worker。

8.13 路径验证

Daemon 对本地路径执行:

  1. 必须是绝对路径;
  2. 清理 ./..
  3. 禁止 /、整段 $HOME/Users/home、系统目录和过宽根目录;
  4. 解析 Symlink 后再次对 Canonical Path 做黑名单检查;
  5. 必须存在且是 Directory;
  6. 用实际读写探针验证进程权限。

为什么 Symlink 后还要再检查?

/Users/alice/project/home-link -> /Users/alice

字面路径看起来安全,真实目标却是整个 HOME。锁也必须使用 Real Path 作 Key,否则两个别名会绕过互斥。

8.14 路径锁

LocalPathLocker 是进程内、按 Canonical Path 的锁:

  • 第一个 Task 成为 Holder;
  • 后续 Task 回调 onWait(holder),将服务端状态改为 waiting_local_directory
  • 等待支持 Context Cancel;
  • 释放函数幂等且清理空 Entry;
  • 获锁后才 Start Provider。

锁是 Daemon 内存级的,因为同一 local_directory 被绑定到特定 Daemon。若允许多个 Daemon 访问共享网络盘,还需要分布式文件锁;当前资源模型没有承诺该用法。

8.15 Sidecar Manifest:如何不污染用户目录

Context/Skill 注入会在 Workdir 写文件。对标准 Env,GC 最终删除整个 Root;对本地目录,绝不能 rm -rf 用户 Repo。

系统记录每个自己创建的文件和目录:

  • Task Context Marker;
  • CLAUDE.md / AGENTS.md 等 Brief;
  • .agent_context
  • Provider Skill 目录;
  • Project Resource 文件。

结束或 Reuse 前调用 CleanupSidecars

  • 删除仍与记录匹配的托管文件;
  • 对被 Agent/用户填入内容的目录保守保留;
  • 不删除未登记文件;
  • Manifest 自身也清理。

这是“记录自己写了什么,再精确撤销”,而不是靠文件名猜测。

8.16 环境复用

同一 Agent/Issue 的后续 Task 可尝试复用旧 Workdir:

  • 路径必须存在;
  • 必须是 Daemon 管理的安全目标;
  • Managed Provenance 与 Workspace/Issue/Agent 匹配;
  • local_directory 走自己的共享目录语义,不使用普通 Reuse;
  • Provider Session 未被 Failure Poison;
  • Context、Skills、MCP 必须重新写入,不能沿用旧快照;
  • 旧 Sidecars 先回滚,再写新 Manifest。

复用的目的不是节省创建目录时间,而是保留代码修改与 Provider 会话连续性。

8.17 Active Root Guard

GC 与任务启动并发时可能发生:

  1. Task 预测将使用某个 Env Root;
  2. GC 同时把它识别为旧目录;
  3. Prepare 正在写文件时,GC 删除目录。

Daemon 在 Prepare/Run 前 markActiveEnvRoot,GC 删除前必须 reserveEnvRootForGC。两者在锁内仲裁,Active Root 无法被 GC 预留。

Codex Session Store 也有类似 Active/Reserve 防护。

8.18 GC 不是只看 mtime

gc.go 读取 .gc_meta.json,按任务种类询问 Server:

  • Issue 是否仍存在、状态是否允许清理;
  • Chat Session 是否删除;
  • Autopilot Run 是否终态;
  • Quick Create/Task 是否仍需保留;
  • Server 不可用时是否只能做保守判断。

可能动作包括:

  • 保留全部;
  • 只清理大 Artifact;
  • 删除 Daemon 管理的 Env;
  • 对孤儿使用 mtime 兜底。

local_directory Override 永远不删除用户 Workdir;即使父对象 404,也只处理 Env Root 中的托管 Scratch/Sidecar。

8.19 GC Meta 为什么最后写

.gc_meta.json 包含 Kind、关联对象、完成时间、是否 Local Directory 等。它只有在:

  • Provider 已结束;
  • 消息/用量尽量上报;
  • Server 终态已处理;
  • Sidecar/结果信息稳定;

之后才写。Meta 的存在表示“可进入业务感知 GC 决策”,不是简单的“目录创建过”。

8.20 本章不变量

  1. 每个 Task 有独立 Env Root;用户目录只是 Workdir 替换,不归 Daemon 所有。
  2. Agent Context 与 Provider Home 必须按任务刷新。
  3. Repo Cache 可共享,对同一 Bare Repo 的变更必须串行;Task Worktree 不共享。
  4. 本地目录必须经过字面路径和 Canonical Path 双重验证。
  5. Sidecar 只删除自己登记的内容。
  6. Active Env/Session Store 不能被 GC。
  7. GC 先查业务状态,再用时间兜底;本地目录永不整目录删除。

8.21 本章结论

执行环境把“Agent 能看到和能写什么”变成了文件系统级契约:

  • 标准 Task 用 Daemon 所有的隔离目录和 Worktree;
  • 特殊 Task 可使用用户本地目录,但要验证、互斥和精确回滚;
  • Provider 的 HOME、Session、Skill 和 MCP 各自按原生语义隔离;
  • 环境可恢复,但恢复必须有 Provenance;
  • GC 只清理经过所有权与业务状态证明的内容。

下一章看这些环境最终怎样交给 17 类不同 Agent CLI,并被压成一个统一运行接口。


上一章:Daemon、Runtime 与任务抢占
下一章:Agent 后端与协议适配