ARTICLE · 1305663
【AX 实战 08】文档说 AX 会把 MCP 和技能包装好,我读完源码发现它只建了个目录
副标题:git、files、MCP、Skills 四类内容的真实边界,与 F11 那个不说就踩的例外
金句:「只准备一次」的真正含义是——成功才打卡,失败下次再来。
上期回顾:第 7 章我们摸清了 Manifest 的两道闸:10 种坏 YAML 当场打回,2 种骗过 apply 再变 Failed。那些反例都围绕 Task 身上的字段。这一章换主角——Workspace,AX 里负责「开工前把家当备齐」的那个 kind。git 仓库、规则文件、MCP 工具、Skills 技能,四类内容全靠它带进沙箱。
本篇要点:
四类内容各解决什么: spec.git/spec.files/spec.mcp/spec.skills的真实职责边界spec.git五个字段全解:clone 到哪个子目录, name/dir/depth的隐藏规则- 文档口径核对
:README 和 concepts.md 说 AX 会「pre-wire」MCP 与技能包——源码只建目录(三处文档说法逐一核实) - F11 例外
:「只准备一次」的 marker 机制;git clone 失败不写 marker,下次启动重试(5 次 × 2s) 多仓库为什么必须合并进一个 Workspace
和多数 AX 文章不同:本章三处「文档与代码不一致」的说法,每一条都当场回源码核实(原文引用 + 文件行号),不含「据说」;F11 的例外从注释、常量到 e2e 测试缺口(M5)一路钉到源码行。
目录锚点:一、四类内容总图|二、spec.git|三、spec.files|四、MCP 与 Skills 的真实边界|五、F11:只准备一次的例外|六、多仓库与单绑定|七、完整案例|八、失败复盘|九、反例/正例|十、自测
前置与难度:第 5~7 章读完即可。难度 ★3(机制不深,但文档误导性最强的一章)。预计 15 分钟。
一、四类内容总图:家当分四类,各走各的门
一个 Workspace 的 spec 只有四个字段(ax.proto:137-141):
spec: git: [] # 代码从哪来 files: [] # 规则写在哪 mcp: {} # 工具怎么接 skills: {} # 技能从哪加载四类内容在 maiden run(首次准备) 里的落点和真实行为:
git | setup.go:114-135 | |
files | setup.go:300-316 | |
skills.path | 只建目录 | setup.go:243-251 |
mcpskills.registries |
顺序有讲究:writeFiles 先于 cloneRepos(setup.go:96-98)。checkout -f 只会覆盖仓库里已跟踪的路径——如果你把文件写进某个仓库将要检出的同名路径(比如根目录的 config.yaml 正好是仓库文件),会被检出内容盖掉;untracked 的新路径不会被删(e2e 的 overlay/config.yaml 就是这么活下来的,见 e2e_test.go:387-388)。稳妥做法:overlay 文件写到仓库外的目录。
二、spec.git:代码从哪来
2.1 五个字段,不止三个
GitRepo 有五个字段(ax.proto:144-150):
repo | ||
branch | main | |
namedir | ||
depth |
大纲只列了 name / repo / branch 三个——dir 和 depth 是源码里真实存在的另外两个,别漏。
2.2 落点决策:name 可能根本不生效
clone 目标目录的规则(setup.go:154-173):
dir优先: "."→ workspace 根;绝对路径 → 原样用;其他 → 相对 workspacedir空 → 用 name;但name为空、"repo"、"origin"时当占位名丢弃,改从 URL 派生URL 派生规则:去 .git后缀取最后一段(setup.go:268-276)
# 例:docs/manifests.md 官方示例name: originrepo: https://github.com/chalk/chalk.git# name=origin 是占位名 → 被丢弃# 目录从 URL 派生 → /workspace/chalk# (不是 /workspace/origin!)这个坑后面「文档核对」再回来算账。
2.3 clone 的实现:init + fetch,不是 git clone
fetchRepo(setup.go:183-215)走的是 git init → remote add(已存在则 set-url)→ fetch origin <branch> → checkout -f FETCH_HEAD。所以:
- 幂等
:失败后重试落在半初始化目录上也能跑( :197注释) - 钉的是分支/tag
,不是 commit—— branch: "my-fix"就 fetchmy-fix depth > 0时 fetch 带 --depth=N(:206-208)参数直接传 exec.Command,不经 shell(:192)
重试:每个仓库失败重试 5 次 × 2s(setup.go:43-44、:132)。重试期间 git-error.log / 成功后的 git-success.log 落在 /ax 下(:39-40)。
三、spec.files:规则写在哪
File 就两个字段:path + content(ax.proto:152-155)。把 AGENTS.md 当「环境里的系统提示」写进来,是官方示例的典型用法(docs/manifests.md:79-84):
files: - path: "AGENTS.md" content: | # Project Guidelines - Run `go test ./...` before submitting changes. - Keep dependencies minimal.三条规则(setup.go:300-316):
path必填—— ValidateWorkspace当场拒绝空 path(types.go:378-384,报错spec.files[i]: path is required)相对路径 → 落在 workspace 下;绝对路径原样用(可以写到 workspace 外,自己负责) - 只在 maiden run 写
——marker 在, writeFiles根本不会执行(setup.go:85-89直接 return)
为什么把 AGENTS.md 当「环境里的系统提示」?因为 agent 一睁眼就能在工作目录里看到它,不需要任何人「注入」——规则跟着环境走,而不是跟着某次会话走。换一个 Task、换一个 agent 框架,只要绑同一个 Workspace,规矩就一样。这就是「环境即代码」在 files 这一项上的含义。
第 3 条规则和 F11 直接挂钩:改了 files 内容再 apply,老 Task 里看不到新文件。要么 delete 重建,要么删 marker 再重启(rm /ax/initialized-*)。这是「只准备一次」最容易被忽视的副作用——它省下的不只是 clone 时间,也锁住了你的规则更新。
四、MCP 与 Skills 的真实边界:三处文档说法逐条核实
这一节是本章的「文档核对」环节。README 与 concepts.md 对 MCP/Skills 的描述比代码做的事多。以下三处说法逐一回源码核实,结论只写代码能证明的。
核实 1|README 说「Pre-wire Git repos, MCP servers, and skill packages」
原文:「Pre-wire Git repos, MCP servers, and skill packages so every agent starts warm」(
README.md:59)
代码现实(setup.go:62-108):maiden run 只做三件事——写文件、clone 仓库、给 skills.path建目录(setup.go:243-251 就一个 MkdirAll)。MCP servers 没有任何连接动作,skill packages 没有任何下载动作。
消费方在哪:mcp 与 skills.registries 原样存进 AX,经 AX_WORKSPACE_YAML 送到任务容器(reconciler.go:195),任务进程可从 metadata 端点 /metadata/v1alpha1/ax/workspace 拿到 YAML(metadata/server.go:122-123)。全仓库检索 Registries 的消费点只有 describe 打印(main.go:637-663)——按 query 发现、拉取技能包的是 agent 自己,不是 AX。
结论:⚠️ 措辞过强。AX 的「pre-wire」对 git/files 成立;对 MCP/Skills 只到「声明 + 建目录 + 透传」为止。
核实 2|concepts.md 说 runner 会 materialize MCP 与 skill registries
原文:「the runner materializes it inside each sandbox before the command starts」,清单含「MCP servers and registries」「Skill registries and the path where skills are materialized」(
docs/concepts.md:26-30)
代码现实:runner 调 SetupWorkspace(runner/runner.go:126),行为同核实 1——skills 只建 path 目录,registries 零动作。「materialized」对 git/files/目录成立,对 MCP/registries 不成立。
结论:⚠️ 与核实 1 同源的措辞问题。「the path where skills are materialized」一句最准确的读法是:AX 保证目录存在,技能包由消费方放进去。
核实 3|manifests.md 示例的 name: origin 是占位名
原文示例:
git: [{name: origin, repo: "https://github.com/chalk/chalk.git", branch: "main"}](docs/manifests.md:75-78)
代码现实:"origin" 和 "repo" 一样是被丢弃的占位名(setup.go:167),目录从 URL 派生为 chalk,不是origin。
结论:⚠️ 示例可跑,但读者若按 name 找目录会扑空。自己写 manifest 时,name 要么不写、要么起个有意义的目录名,别用 origin;最确定的写法是直接给 dir(见 §九 正例)。
五、F11:「只准备一次」的例外
5.1 marker 机制
maiden run 成功后,AX 在 /ax 写一个 marker 文件:initialized-<path>(setup.go:32-35、:256-264)。路径里的 / 换成 -——workspace 挂在 /workspace/primary,marker 就叫 initialized-workspace-primary(e2e 实测断言 MARKERS_OK,e2e_test.go:431)。
下次启动,SetupWorkspace 开头查 marker:存在 → 直接返回,什么都不做(setup.go:85-89)。git 不重新 clone,files 不重写,skills 目录不重建。
5.2 F11 的例外:git 失败不打卡
源码注释写得明明白白(setup.go:66-67):
Git failures are logged and recorded under AXDirbut do not abort setup. The marker is withheld inthat case so the next start retries the clone.行为逐条对齐:
setup.go:129-135 | |
/ax/git-error.log | setup.go:133-134 |
| 不写 marker | setup.go:102-103:106 |
setup.go:43-44:132 |
这意味着:clone 失败的 Task 照样起来(SetupWorkspace 对 git 失败返回 nil,setup.go:102-103,于是 runner 侧走 SetWorkspaceReady(true),runner/runner.go:126-131——这是「失败不中止」的直接后果,而非缺陷),但沙箱里缺仓库。下一次 runner 冷启动(新 Task、或重新创建的容器)会重新走 maiden run,再 clone 一次。
写作与使用口径(FACTS F11):讲「只准备一次」必须带这个例外——否则读者会以为失败也被标记、永远不再 clone,从而误判「改好 URL 后什么都不用做」。实际上:改好 URL 后要触发一次新的冷启动。suspend → resume 是否属于「新的冷启动」取决于 resume 走快照恢复还是重建容器(M4/M5 待实测,别打包票)。
M5 缺口(诚实声明):e2e 只测成功路径(TestE2E_WorkspaceSetup_FilesAndGit,e2e_test.go:408-438),失败重试无实测用例(FACTS M5,静态先证 ✅ / 实跑 ⬜)。源码承诺「会重试」,眼见为实要等 M5。
六、多仓库与单绑定:为什么必须合并进一个 Workspace
Task 绑定 workspace 的字段 WorkspaceRef 只有 name + path 两个字段,没有列表(ax.proto:106-109)——一个 Task 至多绑一个 Workspace(docs/manifests.md:40-42)。
要两个仓库?声明在一个 Workspace 的 spec.git 里:
spec: git: - name: backend repo: https://example.com/org/backend.git - name: frontend repo: https://example.com/org/frontend.git落点就是两个子目录:/workspace/backend、/workspace/frontend(name 不是占位名时直接当目录名,setup.go:165-166)。
为什么这么设计:workspace 的挂载路径就是命令的工作目录(第 6 章讲过的一 cwd 原则)。两套环境 = 两个 Task,各自绑各自的 Workspace——这正好接上第 6 章的扇出模型。
七、完整案例:把团队环境固化成一个 Workspace
以下 manifest 改写自 e2e 测试的 workspaceSetupManifest(e2e_test.go:375-401,字段与落点全部实测过):
apiVersion: ax.io/v1alpha1kind: Workspacemetadata: name: team-envspec: git: - name: primary dir: "." repo: https://example.com/org/mono.git branch: v1.0.0 depth: 1 files: - path: overlay/config.yaml content: "mode: git-plus-overlay" - path: AGENTS.md content: | - Run tests before submitting. mcp: registries: - provider: google query: "mcp.tags:build" servers: - name: git-tools endpoint: "http://git-mcp.default:8080" skills: registries: - provider: google query: "skills.tags:nodejs" path: /workspace/primary/.agents/skills---apiVersion: ax.io/v1alpha1kind: Taskmetadata: name: dev-taskspec: debug: true workspace: name: team-env path: /workspace/primary这份 manifest 不是拍脑袋写的——四类字段各就各位,落点全部对应 e2e 断言:
仓库 clone 到 /workspace/primary(dir: "."),tagv1.0.0,浅 fetchoverlay/config.yaml写在仓库外,不被 checkout -f覆盖skills 目录 /workspace/primary/.agents/skills被创建(断言SKILLS_DIR_OK)marker 是 /ax/initialized-workspace-primary(断言MARKERS_OK)mcp与 skills.registries原样进 metadata 端点,等 agent 自己消费
验收序列:apply → resume → ax ssh dev-task -- ls /workspace/primary 看仓库在不在 → cat /ax/initialized-workspace-primary 看打卡记录。一次跑通之后,这份 Workspace 就是团队的环境基线——新成员、新 Task、新框架,绑上即用。
八、失败复盘:三个真会踩的坑
坑 1:clone 失败,Task 看起来一切正常
SetupWorkspace 对 git 失败返回 nil,WorkspaceReady 照样 True。你 ssh 进去才发现仓库目录是空的。排查路径:ls /ax/git-error.log → 里面有 error 和 git 输出 → 修 URL/网络 → 触发一次冷启动(delete+apply 最稳)→ 下次 maiden run 重试。别盯着 marker 等它自己好——它没写 marker,等的是重启。
坑 2:改了 files,老 Task 看不到
marker 在,writeFiles 不执行。改规则文件的正确姿势:delete 旧 Task → apply 新的。如果连 Workspace 的 spec 也改了,注意 marker 是按挂载路径打点的(setup.go:256-264)——同一路径的 marker 还在,新 spec 同样不生效,要么换新路径,要么 rm /ax/initialized-* 清掉再启动。
坑 3:按 name: origin 找目录
官方示例的 name: origin 是占位名,目录实际叫 chalk(核实 3)。找不到仓库先 ls /workspace,再对照 setup.go:154-173 的决策树。
九、反例 / 正例
反例(照抄官方示例然后猜目录):
git: - name: origin repo: https://github.com/chalk/chalk.git# 猜目录:/workspace/origin → 不存在# 实际: /workspace/chalk正例(要可控就用 dir):
git: - dir: chalk repo: https://github.com/chalk/chalk.git# dir 优先,落点确定:/workspace/chalk十、自测 3 题
- git clone 失败后,marker 写了吗?下次会发生什么?
答:没写( setup.go:102-103)。本次启动照常跑(可 ssh,缺仓库);下一次启动重走 maiden run,单仓再试 5 次 × 2s。 mcp.registries里的query是谁消费的?答:agent 自己。AX 只存声明、透传 YAML(metadata 端点),不做发现和连接。 - 改了
spec.files的内容,已运行的 Task 会更新吗?答:不会。maiden run 一次定终身;要 delete 重建或清 marker 重启。
十一、一页速查
dirname > URL 派生 | |
name: origin | |
一句话小结
Workspace 把「开工前的家当」变成声明:git 真 clone、files 真写入、目录真创建;MCP 与 registries 只透传——而「只准备一次」的打卡,只记成功。
十二、下期预告
第 9 章|Model 与凭证:为什么密钥不该出现在 Task 里。provider / model / parameters 三件套,F14 的参数键名陷阱(proto 说 maxOutputTokens,文档示例写 maxTokens),以及 secretKey 两步创建——把 token 从 config 里救出来。
互动
你是怎么发现 clone 失败的?投票:A. ssh 进去看到空目录 B. 先撞上 /ax/git-error.log C. 根本没发现,agent 自己瞎编了答案。评论区报个字母,选 C 的多就单独开一篇「F11 失败现场」。
事实锚点:AX
4b7bf6f/ Substratev0.6.0适用边界:基于上述锚点撰写;AX 为 pre-1.0,行为可能漂移,以你实际部署的版本为准。
附:标题候选库(留档,供 §6.4 多平台改编复用)
★ 公众号推荐标题(系列统一定稿):【AX 实战 8】文档说 AX 会把 MCP 和技能包装好,我读完源码发现它只建了个目录 副标题(技术定位):git、files、MCP、Skills 的真实边界















































































