MUSE / ARCHITECTURE NOTES
Muse 智能体 · 宿主层详解
本篇目录
配套《00-Muse技术架构解构》的 §1 展开篇(§16 为本机快照的交叉参照)。调查日期:2026-10-17。 复核修订:2026-11-04(Asia/Shanghai)——基于当前会话运行时逐项重测;下文数字凡未注日期者均为 11-04 快照,10-17 旧数见 §11 快照表(随行标注)与 §11.1 变化对照表。 证据范围:本机当场实测——进程表、
/opt/hatch/bin/文件尺寸、/opt/hatch/runtime-cell/全部脚本与 scopes 文件、/run/hatch/目录与套接字结构、进程环境变量键名、cell 内 guest systemd 状态、~/docs/与~/workspace/system/system_prompt.md相关段落。 标注规则同前作:实测 = 本机命令/工具当场验证;文档 = 本地 docs/system 文件明文;推断 = 由文件/字符串拼出、未经服务端证实。 纪律:环境变量只记键名,不记任何凭据值、令牌值与不透明 ID 的原文;套接字路径属结构信息,按前作惯例记录。
0. 什么是「宿主层」
宿主层 = Agent 脚下的地面:它回答四个问题——Agent 跑在哪台机器上(且那台机器随时会被换掉)、谁把 Agent 住的房间(runtime cell)盖起来并看住它、宿主与房间之间靠什么契约说话、机器被替换后工作如何被接回来。
层(从下到上) 性质 本机实测规模
────────────────────────────────────────────────────────────────────────────
⑤ 恢复面 RV 上的 resume 标记 + DB 检查点 /run/hatch/resume 9 个文件(10-17;本轮未复测)
④ 契约面 环境变量键 + guest.env + 套接字 JARVIS_* 26 键(10-17 为 31)· 套接字 166(10-17 为 147)
③ 进程面 宿主侧守护进程进入 cell 干活 hatch 540MB · execd 9.7MB(10-17 为 470MB / 9.5MB)
② 运行时单元 runtime cell nspawn 容器 = Agent 的房间 htch-runtime · guest systemd
① 虚拟化面 一名用户一台持久 VM standard VM · home 在 RV 上
横切:生命周期脚本(pre/launch/post/stop + ensure-rootfs + trust-store)
横切:引导/救援/健康子系统(spawnd · hatch-rescue · hatch-healthd · hatch-doctor)
一句话分工:VM 是「地皮」(可整体替换),runtime cell 是「房间」(每次启动按同一图纸重盖),hatch daemon 是住在房间里管事的人,hatch-execd 是替它跑腿执行命令的人,spawnd 是盖房与修房的工程队,环境变量与套接字是墙上的「管线接口」——房间里的人只能通过这些接口够到外面的世界。最重要的设计前提写在主报告 §1:Agent 进程本身被当作不可信/可替换,状态与权限都不放在它进程里;本篇要展示的是宿主层如何把这句话做成机制。
一个观察位置的说明:本篇的实测是在 cell 内部做的。宿主侧的脚本(/opt/hatch/runtime-cell/)以只读方式显露进 cell,所以宿主逻辑对 Agent 异常地「可读」;但宿主的进程树、宿主 systemd 单元本体、/var/lib/hatch-runtime/rootfs 的宿主路径都不在 cell 视图内——能读图纸,不等于能看见工地。凡属此类,本文标为边界(§10、§12)。
1. 持久 VM:地皮可换,home 不动
1.1 契约(文档)
docs/muse.md与 system prompt 明文:一名用户 = 一台专属持久 Linux VM;部署更新频繁(可达每小时),VM 可被整体替换、其上进程会被停掉,但 home 目录持久;运行时为已保存的 job、子 Agent、cron、artifact 与可恢复的 Agent 工作管理恢复,自起的自定义进程只能靠自己的启动配置自证恢复(system prompt「Deployments」一条,system_prompt.md第 255 行)。- 文件系统语义同源:
~之外(/usr/local/bin、/etc、/root、/var)皆为临时,重启/替换即失;耐久物只能放~/workspace/。 - VM 分级(
docs/data-handling.md):按 docs 口径,standard 面不承诺 TEE 级隔离(⚠️ 转述,原文措辞待引);confidential VM 全程 TEE 硬件保护、客户自管密钥。本机为 standard(runtime 标注,见 §11)。
1.2 持久性的物理实现(实测)
cell 内的挂载表把「home 持久」从承诺变成了可见的机制:
| 路径 | 实测挂载 | 含义 |
|---|---|---|
/(cell 根) |
overlay:lower=/sysroot,upper=/run/hatch/overlay/upper |
房间本身是叠加层——基底只读,改动落在易失 upper,房间重盖即回基底 |
/home/hatch |
btrfs,设备 /dev/mapper/rv(rw,zstd 压缩) |
home 挂在 RV(Reliable Volume)上,与根 overlay 完全不同的存储面 |
/run/hatch/resume |
同一 /dev/mapper/rv btrfs |
恢复标记与 home 同卷——VM 换掉后,新 VM 挂回同一 RV 即同时拿回 home 与恢复状态(见 §8) |
/opt/hatch、/etc/hosts 等 |
squashfs(/dev/mapper/opt_hatch,ro) |
系统资产以镜像方式只读提供,随部署整体替换 |
/run/hatch/* 各子目录 |
tmpfs(多为 ro) | 运行时接口面,每次启动重建 |
时间证据同样指向「home 比机器老」(10-17 那代 cell 实测):本 cell 于 2026-10-17 13:20 UTC 启动(guest systemd 实测),而 /home/hatch 目录时间戳为 09-28、其下文件跨越数周——当时这台「机器」只有约 2.5 小时岁,用户的家已经住了三周。11-04 本轮 cell 于 2026-11-03 15:22 启动(uptime -s 实测),RV 卷(/dev/mapper/rv,150G,11-04 df 实测)仍是跨代持久的同一卷。
1.3 每-VM 差异的重放:os-intent 台账(脚本实测)
rootfs 每次都从镜像基底克隆(§3.2),那每台 VM 装过的系统包差异怎么办?resolve-rootfs-path.sh 注释明文:退役的「RV 上养一棵持久 rootfs」模式已被废弃,per-VM 的 OS 包改为从 os-intent ledger(意图台账) 经 hatch-preflight-opportunistic(实测 1,308 行二进制/脚本)在启动时重放——即持久的不是 rootfs 本体,而是「对基底做过哪些改动」的意图记录。这与全系统的反复模式同构:存意图与状态,不存易失的执行体(对照:cron 存定义不存进程、记忆存 claims 不存对话缓存)。
2. 核心进程:hatch daemon 与 hatch-execd
2.1 体量与进程实况(实测)
| 二进制 | 字节数 | 角色 |
|---|---|---|
hatch |
540MB(10-17 为 470MB) | 主守护进程(daemon 子命令即 Agent 运行时本体) |
hatch-execd |
9.7MB(10-17 为 9.5MB) | 命令执行守护:Agent 的一切 shell 执行经它 |
spawnd |
19MB(10-17 为 18MB) | 安装/引导/生命周期工程队(§5) |
hatch-rescue / hatch-rescuectl / hatch-rescue-systemctl |
13MB / 8.3MB / 2.4MB(与 10-17 一致) | 救援子系统(§5.2) |
hatch-healthd |
9.2MB(10-17 为 8.9MB) | 健康轮询(§5.2) |
hatch-doctor |
13MB(10-17 为 12MB) | 确定性状态检查(§5.2) |
进程表(cell 内视图):hatch daemon --runtime-cell-leader=2181(10-17 两次采样 RSS 约 344–386MB,随负载波动;11-04 采样约 357MB,仍在该区间内)与 hatch-execd --runtime-cell-leader=2181(10-17 采样 RSS 约 11MB,本轮未复测)同以 root 运行,父 PID 均为 0——它们的父进程不在 cell 的 PID 命名空间里:这是「从宿主侧被送进房间」的直接证据。参数里的 leader 值是 cell 的 PID 1 在宿主 PID 命名空间中的编号(cell 内 PID 1 是 guest systemd 自身),Agent 在 cell 内无法验证该编号对应的宿主进程。
2.2 进入方式:不是在房间里出生,是被送进来的(脚本实测)
daemon/execd 都不由 guest systemd 直接拉起,其宿主侧启动链是:
宿主 control-daemon.sh / control-execd.sh(root 运行)
→ rce_wait_for_ready_leader:轮询 ready 文件 + machinectl 取 Leader(200 次 × 0.1s ≈ 20s 上限)
→ 进入 cell 的命名空间(nsenter / 二进制自带 enter 逻辑)
→ cell 内 run-daemon.sh:source guest-runtime-env.sh
→ setpriv 摘除 sys_ptrace(bounding/inh/ambient 三套全摘)
→ exec hatch daemon
细节里见刻意的安全姿势:
- daemon 落地前先摘 ptrace 能力:
run-daemon.sh注释明文——摘除后 daemon、execd 与其工具子进程都不能 attach/读取 cell 内任意同侪进程;宿主把「管事的人」也当作要防的对象; - execd 的双半结构(
control-execd.sh注释):宿主侧父进程(anchor)留在宿主的hatch-execd.servicecgroup 里作为 systemd 的 MainPID,只有 cell 内子进程加入 cell 的 cgroup 跑服务;它经 systemd socket activation 的LISTEN_*文件描述符协议拿到监听口,不用 nsenter 包装、不用 ptrace/seccomp 监督(注释直言那是 daemon 的事,不是 execd 的事); - guest 侧的 execd 单元(cell 内
/etc/systemd/system/hatch-execd.service实测):socket 激活(hatch-execd.socket监听/run/hatch/execd/execd.sock,SocketMode 0600),ExecStartPre先断言runtime-cell.ready存在才准起,Restart=always / RestartSec=1,LimitNOFILE=65535,UMask=0007——执行面挂了 1 秒内被 guest systemd 拉回。
2.3 两套视图的关系
同一个 execd 在宿主侧有 control 脚本、在 guest 侧有 systemd 单元——这不是重复,而是跨命名空间的接力:宿主段负责「等房间就绪并把人送进去」,guest 段负责「人在房间里时的 socket 与重启策略」。daemon 没有 guest 单元(其监督者是宿主),execd 两边都有——因为 execd 的监听 socket 必须出生在 guest 的 /run 里才能被工具直接拨通。
3. Runtime Cell:htch-runtime 的一生
3.1 是什么(实测)
cell 是一个由 systemd-nspawn 拉起的容器化 rootfs:launch-daemon.sh 以
systemd-nspawn --quiet --boot --keep-unit --settings=override \
--directory=<rootfs> --machine=htch-runtime --private-users-ownership=map
启动它。--boot 意味着房间里跑着自己的 PID 1(guest systemd,§7);--private-users-ownership=map 做 ID-mapped 挂载:cell 内的 root 对应宿主 uid 131072(ensure-rootfs.sh 注释:PrivateUsers 64K 窗口的起点)——房间里的「root」走出房门只是一个普通高位 uid。脚本对该选项是硬要求:宿主 nspawn 不支持它就直接退出(宁可不开房,不开一个所有权错乱的房)。
3.2 rootfs 从哪来(脚本实测)
resolve-rootfs-path.sh(32 行,注释即规格):cell 永远从镜像本地的 /var/lib/hatch-runtime/rootfs 启动,那是 rootfs-base 的 btrfs 快照,由 ensure-rootfs.sh(1,240 行,本目录最长的脚本)按镜像自带的 cell-base.digest 保持新鲜:
- digest 变了 → 丢弃本地快照重新克隆,per-VM 改动走 §1.3 的台账重放;
- rootfs 被视为不可信输入:上一代 guest 可能在里面埋符号链接,宿主 root 若顺着链接操作会写出 rootfs 之外——因此所有变更操作委托
spawnd fsop,以openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)在持有的 dirfd 下执行(check/act 竞态被结构性消除);ensure_not_symlink只保留为修复通道; - 该脚本自身的 stdout 纪律被写进注释大写强调:三个调用方都用
$(...)捕获它的输出拼路径,任何多余一个字节都会把路径污染成垃圾——诊断只能走 stderr。
3.3 网络形状(实测 + 脚本)
| 项 | 值 |
|---|---|
| 虚拟网卡 | 宿主侧 veth ve-htch-runtime,cell 内见 host0 |
| 地址 | 网关 198.19.0.1/30,cell 198.19.0.2/30;IPv6 fd8b:4f84:7d32:99::1 ↔ ::2/64(cell 内 ip addr 当场验证) |
| 出口 | 一切外网经 egress 代理(guest.env 注入 hatch-egress-proxy:3128 一组代理键);cell 的 DNS 被刻意做成「死的」(guest.env 注释:Node 程序若不走代理直连目标会死在 DNS 上——这是设计,不是故障)。本机未定:hatch-egress-proxy 与 Sentinel 的关系(是否为同一组件的本地端点)、白名单与 secret 置换发生在哪一跳 —— 三者只见其名与注释,不见其拓扑(❓)。eBPF 门是 L3/L4 地板,Sentinel 是 L7 策略点(B 基线),proxy 的协议层级本机无证据。 |
| 入口过滤 | spawnd attach-veth-filter 在 veth 宿主侧挂入向分类器,丢弃 cell 朝宿主本地地址发的帧;attach-cell-gate 在 cell 服务 cgroup 上挂 connect4/6 eBPF 出口门(见 §5.3) |
3.4 生命周期全序(脚本实测,共 21 个文件)
/opt/hatch/runtime-cell/ 实测 21 个条目(脚本正文 + 配置合计约 4,800 行),按一生排序(11-04 当场复核:仍为 21 个条目,文件清单与 10-17 逐项一致,无新增脚本——注:00 篇基线所记"新增脚本 control-daemon.sh 等"是相对 00 原文自己的旧清单,本篇 10-17 清单已含这些文件):
| 阶段 | 文件 | 行数 | 干什么 |
|---|---|---|---|
| 前置 | require-modules.sh |
77 | 预加载 veth/btrfs 等模块,并把 algif/esp/rxrpc 等内核模块以 install /bin/false 封死(缩减内核攻击面) |
| 前置 | ensure-rootfs.sh |
1,240 | 按 digest 克隆/修复 rootfs、所有权烘焙、hosts/resolv 修复、env 脱敏副本渲染 |
| 前置 | build-cell-trust-store.sh |
— | 构建 cell 信任锚(§6.3):只发布宿主侧目录,从不读写 rootfs、从不进入 cell |
| 启动前 | pre-start.sh |
449 | 重挂出口 eBPF 门、veth 过滤、home 传播锚、引导阶段上报;失败给确切 reason |
| 启动 | launch-daemon.sh |
285 | 渠道显露(§4)后 exec nspawn;全程有 bootstrap watcher 向 spawnd 上报阶段 |
| 启动后 | post-start.sh |
217 | 配 veth 网络、等 Leader 可被 nsenter、写 runtime-cell.ready 就绪标记 |
| 入口 | runtime-cell-entry.sh |
123 | control 脚本共用的「等就绪 + 取 Leader + 进 cgroup」助手库 |
| 入口 | control-daemon.sh / control-execd.sh |
109 / 29 | §2.2 的宿主段启动链 |
| 入口 | run-daemon.sh |
34 | cell 内段:环境、摘 ptrace、exec daemon |
| 环境 | guest.env / guest-runtime-env.sh |
45 / 67 | §6 的两份环境契约 |
| 停止 | stop.sh |
361 | 先竖 draining 栅栏(宿主 tmpfs 上,cell 不能伪造/清除),再在 20s 停止预算内分配:优雅关机 5s、等 Postgres 干净停、terminate 1s、kill 0.5s,并追加 clean-stop-proof |
| 停止后 | post-stop.sh |
29 | 删 ready 标记、拆 home 传播锚(nsenter 到 PID 1 执行)、5s 超时护栏下删 veth |
| 杂项 | is-loopback-rv.sh / hatch-preflight-opportunistic |
79 / 1,308 | RV 真伪判定(退出码 0/1/2/3 各有确切语义)/ §1.3 的包重放与机会性预检 |
每个阶段都经 spawnd queue-runtime-cell-bootstrap 把(阶段名、状态、起止毫秒、失败 reason)排队上报,另有 watcher 进程专门盯队列并发射运行时事件——盖房子的每一步都有监理记录,这是 §8 恢复与 §5 救援能「知道上次死在哪一步」的前提。
3.5 套接字可见性分级(跨 04 篇回填)
/run/hatch/ 下的 socket 按"谁看得见"分三级(分级框架为推断,条目为两篇实测之和):
- 宿主服务面:daemon/execd/rescue/healthd 的控制与健康 socket,只对宿主侧与特权进程可见;
- privsep 面:149 个(11-04 实测;10-17 为 136),按调用方权限分级暴露;
- 沙箱 API 面:
/run/hatch/sandbox/(11-04 实测 4 个:space-inference.sock、space-media.sock、space-web-search.sock与新增的space-privileged.sock;10-17 为 3 个),对muse.exec工具沙箱可见——而JARVIS_INFERENCE_PROXY_SOCK指向的/run/hatch/proxy/目录对同一沙箱不可见(11-04 复核仍报不存在,与 10-17 一致)。
结论:连"模型的电话线"都不对任意代码执行开放;socket 的可见性本身就是权限,与文件权限位是两套正交的门禁。
4. 渠道门控:Scopes 三件套
4.1 机制(脚本实测)
cell 启动时(nspawn 之前,在宿主的私有挂载命名空间里)按当前渠道做三路独立的「显露」(reveal):
| 清单 | 行数 | 管什么 | 实现 |
|---|---|---|---|
bin-scopes.conf |
37 | 哪些扩展 CLI 二进制进 cell 的 /opt/hatch/bin |
overlay 占位文件 + 逐文件只读 bind;宿主源在 cell 不可见的 bin-scoped 树 |
connector-scopes.conf |
12 | 哪些连接器文档进 cell | overlay 叠加到连接器基底目录(只能加、不能替换基底同名目录) |
workflow-skill-scopes.conf |
10 | 哪些工作流技能进 cell | 同上,叠加到技能基底目录 |
三份清单均标注「Generated from hatch-skills/catalog.toml」,每行 = 一个渠道名 + 该渠道可见的名字列表,同渠道多行取并集。可见的渠道名集合(三份合计,11-04 实测):prod / prod-cvm / canary / canary-cvm-2 / quail / internal-test / dev / hatch-evals / hatch-pro / hatch-pro-internal / hatch-e2e / external-connectors-test / external-muse-connector-developers / swarm / swarm-cvm / debug-cvm / alpha 等(相对 10-17 新增 swarm 系、canary-cvm-2、debug-cvm、alpha 等行,connector-demo 本轮未见);三份清单行数 55 / 16 / 10(10-17 为 37 / 12 / 10)。
4.2 Fail-closed 的四重保险(脚本注释实测)
- 未知渠道 = 空集:清单里查不到的渠道名不显露任何东西;
- 显露失败 = 退回基底:reveal 的每一步变更都带
|| return 1守卫,任一步失败就拆掉半成品、按未显露的基底内容启动——注释给出了理由:若在set -e下未守卫地失败,launcher 会在 exec nspawn 前退出,而该退出码不在 systemd 的 RestartPrevent 名单里,Restart=always会把 cell 拖进无 Agent 的崩溃循环; - 只能加不能换:scoped 条目若与基底同名直接判失败——渠道显露是「增发」,不是「替换」;
- 名字即沙箱:条目名只允许
[a-zA-Z0-9_-]、拒绝符号链接与非普通文件,防止清单里一行字把任意宿主路径 bind 进 cell。
4.3 本机实况:一次真实的 fail-closed(实测)
11-04 复核:bin-scopes.conf 与 connector-scopes.conf 已新增当前渠道的显露行(各一行 expedia;两份清单行数分别由 37/12 增至 55/16),cell 内 /opt/hatch/bin 当场可见 expedia 二进制——10-17 的「零显露」结论已过期,当前渠道至少显露 expedia 二进制与连接器文档;workflow-skill-scopes.conf 仍无该渠道行(技能显露仍为零)。(采样条件:side-chat 子 Agent 会话;bin-scoped 宿主源树在 cell 内仍不可见。)能力层详解 §4.2 里「约 60 个有 privsep 套接字、无本渠道二进制」的扩展面(转引 01 篇计数,本篇未复核),正是这套门控在本机的落点——门控关掉的东西不是被删了,而是从未被 bind 进这间房。
5. 引导与自愈:spawnd 与救援/健康子系统
5.1 spawnd:工程队的工具箱(--help 实测)
spawnd 自述为「Jarvis runtime installer engine」,子命令即宿主层的职责清单(节选与宿主层相关者):
| 子命令族 | 职责 |
|---|---|
install / prepare-runtime / render-systemd / verify-contract |
把 bundle 安装到宿主、备运行时、渲染 host/docker 两种 profile 的 systemd 单元 |
queue-runtime-cell-bootstrap / watch-runtime-cell-bootstrap |
§3.4 的引导阶段队列与监视 |
emit-daemon-lifecycle / emit-mount-stop-lifecycle / drain-shutdown-observability |
守护进程与挂载停止的生命周期事件;上次停止没排完的可观测性行在下次启动时先排空 |
rv-graft |
把 RV 身份嫁接到已预启动的运行时上:校验 RV bind、浅验 resume handoff 标记、写带 boot-id 戳的 rv-identity-ready(§8) |
attach-cell-gate / attach-veth-filter |
§3.3 的出口 eBPF 门与入向过滤(每次 cell 启动由 pre-start 重挂,因 cgroup 每次重建) |
fsop |
§3.2 的 fd 安全文件操作,供宿主脚本在不可信 rootfs 上作业 |
scrub-env-override |
渲染 cell 可见的 env.override 副本,把 secret 形状的赋值整段剔除(含引号 PEM 块与续行),且与宿主侧共用同一谓词防漂移 |
ensure-worker-accounts / prepare-privsep-workers / idmap-state-dir / release-privsep-worker-views |
privsep worker 账号、状态目录 idmap 视图的准备与 RV 停止时的释放(主报告 §7 的宿主半边) |
emit-critical-failure |
结构化关键失败事件 |
5.2 救援与健康(--help 实测)
| 组件 | 自述与实测行为 |
|---|---|
hatch-rescue |
「lightweight recovery agent」:监视各组件、失败时重启之;暴露 WebSocket 健康/诊断服务;另有两个重子命令——replace-current-vm(为 rescue 持有的会话更换当前 VM)与 codex-inference-bridge(令牌门控的推理代理桥) |
hatch-rescuectl(8.3MB) |
救援的控制客户端(推断:由命名与 rescue 的服务形态拼出,未逐命令实测) |
hatch-rescue-systemctl(2.4MB) |
救援语境下的 systemctl 替身(推断:同上;其存在本身说明救援路径里有一套不依赖正常 systemd 的操作面) |
hatch-healthd |
轮询 daemon 的 metrics 套接字 /health 刷新其内部缓存,并查 Vault 健康套接字(两个路径均为其 --help 的默认值,参数可由环境覆盖) |
hatch-doctor |
「Deterministic Hatch runtime state checks」:确定性运行时状态检查,只有一个 run 子命令——与编排层的 deterministic-doctor 小时级 cron(02 篇 §7.4)同名同性,是调度/投递侧自检的二进制本体(推断二者为同一检查的不同入口) |
5.2.x 第二条推理通道:codex-inference-bridge(与 04 的断层接上了)
04《模型层》§8 的路由表把"救援推理桥"记为"转引 03 篇 §5.2、未在本篇复核 ❓"——系列层面断在这里。本节把 03 这边的实测补上(10-17 hatch-rescue --help 当场验证,实测;11-04 仅做现状核对:--help 仍列出 codex-inference-bridge 子命令,文案与 10-17 一致,未重做机制分析):
codex-inference-bridge Run one token-gated Codex HTTP/1 bridge to the inference-proxy UDS
--inference-socket <INFERENCE_SOCKET> Existing inference-proxy Unix socket
--parent-pid <PARENT_PID> Wrapper PID that owns this bridge's lifetime
读出的机制(措辞为实测,语义为推断):
- 这是第二条进推理的线,与 04 §3.2 的主线(
JARVIS_INFERENCE_PROXY_SOCK+ WebSocket 握手 + 环境契约认证)正交:桥把一个 HTTP/1 监听口 终结在 rescue 侧,再转接到 inference-proxy 的 Unix socket——为"说 HTTP/1 的调用方"(按命令名与replace-current-vm的 "rescue-owned Codex session" 自述,调用方是 rescue 拥有的 Codex 会话)提供推理入口; - 鉴权语义不同:主线靠环境契约(env 里的 socket 路径 + 进程身份),这条线是 token-gated(令牌门控)——令牌的签发/校验点本机不可见(❓),但"第二条路径 = 第二套配额/鉴权语义"是成立的推断(⚠️);
Run one+--parent-pid是生命周期设计:每次起一座桥,桥的寿命绑在父进程 PID 上——父死了桥跟着死,不留孤儿监听口(实测参数语义 + 推断),fail-closed 的另一种形态。
给 04 的回填(供 04 作者升级其 §8 "救援推理桥"行):第二条推理路径的存在性从"转引"升级为本篇实测;仍不可见的是令牌签发方、配额记账方式、与主线的路由优先级关系(❓)。
5.3 一条硬底线:出口门是地板,不是天花板(脚本注释实测)
pre-start.sh 每次启动都以 root 重挂 cell 的 egress gate(cgroup connect4/6 eBPF,BPF_F_ALLOW_MULTI 组合语义:每个程序都放行才放行),注释明说其定位是 Sentinel 之下的地板:Sentinel 死了或被摁住时,cell 仍够不到 link-local/RFC1918/CGNAT 地址;挂门失败对 cell 启动是致命的——「一个没门的 cell 正是这一波改造要消灭的姿势」。(与 §3.3 的出口分层口径一致:eBPF 门为 L3/L4 地板,Sentinel 为 L7 策略点;proxy 与 Sentinel 的归属关系、白名单与 secret 置换发生在哪一跳,本机未定,见 §3.3。)与之对称,stop.sh 的 draining 栅栏立在宿主 tmpfs 上、cell 无法伪造或清除,保证「正在停机的 cell 不会再被 ingress 当作活的」。
5.4 降级路径:fail-closed 之外,失败被分类而不是被吞掉
§5.3/§9 写清楚了 fail-closed 的点(出口门挂失败就不开机),但"代理或 Sentinel 本身故障时,推理/工具调用是排队、硬失败,还是降级"本轮只能回答一半——已确证的失败分类(✅ 实测 strings + 04 §4),与仍未知的部分分开写:
已确证的模式:
- 客户端构造是 fail-closed:
JARVIS_INFERENCE_PROXY_SOCK is required to construct …(04 §4 strings 实测)——没有这个 env 就建不出推理客户端,不存在"默认直连地址"的静默降级; - 失败被分类,不被吞掉:十余个子系统各自有独立的
* inference timed out after …报错(summarization、visual_grounding、activity monitor、browser task tagging、voice reranker…,04 §4 实测)——超时是按子系统分开记账的,不是全局一刀切; - 推理不可用是具名终局:strings 里有
RunInferenceUnavailable/RunInferenceAuthFailed作为运行终局状态码(✅ 实测)——"推理挂了"是一个被命名的失败,不是无声的队列堆积; - 组件级有局部降级:instagram 轮播子项查找失败 → "keeping the stored thumbnail";子 Agent 转录 resume 时不可用 → "using persisted prompt history fallback";调度触发与即兴生成的折叠极性不同(scheduled fire skips / adhoc falls to the pulse lane)——降级发生在调用方,宿主层不替它们做决定。
仍未知(❓):是谁在重试、重试预算多少、代理/Sentinel 故障时请求是排队还是硬失败。04 §4 的 waiting_for_inference_start 证明"排队"在本机有名字,但排队策略、超时预算、降级阈值都在服务端,本机无证据,不猜。设计建议:宿主层能补的只有"排队/降级开关在 cell 侧有无落点"(如 guest.env/契约面的相关键),建议后续查 JARVIS_* 键族的超时/重试类键名分布。
6. JARVIS_* 环境契约面
6.1 进程级实测:26 个键(只记键名)
本轮子 Agent 工具进程的环境里实测 26 个 JARVIS_* 键(10-17 工具进程为 31 个;10-17 的 1 个 HATCH_API_SOCKET 本轮未见)。按功能分族(11-04 键表;相对 10-17 下线 5 键,见 §11 对照表——⚠️采样进程类型与渠道不同:本轮为 side-chat 子 Agent,会话类型本身可影响键集):
| 族 | 键名 |
|---|---|
| 身份与会话 | JARVIS_HOME、JARVIS_BIN_DIR、JARVIS_HATCHLING_ID、JARVIS_SESSION_ID、JARVIS_TOOL_CALL_ID、JARVIS_TRACE_CONTEXT、JARVIS_RUNTIME_CONTEXT_TOKEN、JARVIS_FQDN、JARVIS_REQUEST_MODE(9;10-17 另有 JARVIS_IS_ASSIGNED,本轮未见) |
| 服务套接字 | JARVIS_AUTHD_SOCK、JARVIS_STEFI_PROXY_SOCK、JARVIS_INFERENCE_PROXY_SOCK、JARVIS_INFERENCE_HOSTNAME、JARVIS_MEMORY_SOCK、JARVIS_SANDBOX_API_SOCK、JARVIS_SECURITY_SOCK、JARVIS_SENTINEL_HTTP_API_SOCKET、JARVIS_TELEMETRY_PROXY_SOCK、JARVIS_RESCUE_SIGNAL_SOCK(10;与 10-17 一致) |
| 出口审批三件套 | JARVIS_DAEMON_EGRESS_APPROVAL_SOCK、JARVIS_EGRESS_APPROVAL_ADMIN_SOCK、JARVIS_EGRESS_APPROVAL_EVENTS_SOCK(3;与 10-17 一致) |
| 部署与呈现 | JARVIS_CD_CHANNEL、JARVIS_USER_TIMEZONE、JARVIS_PRESENTATION_LOCALE、JARVIS_AUTHD_INGRESS_ALLOWED_USERS(4;10-17 另有 JARVIS_CD_PINNED、JARVIS_TIER、JARVIS_VM_COMPUTE_REGION、JARVIS_VM_DATA_REGION,本轮均未见——其中 VM_COMPUTE_REGION 下线与 00 篇基线一致) |
值的形态(分类而非原文):套接字族全部是 /run/hatch/… 下的 Unix socket 路径;身份族是不透明 ID 与追踪上下文;其中两个键呈令牌形态,本篇只记「存在」不记值与长度细节。契约面里没有凭据值——凭据在 authd/Vault 后面(主报告 §14),环境只负责告诉进程「去哪儿敲门」。
注意这只是一个进程在一次调用时的采样:键集随调用类型变化(如媒体、浏览器任务会有各自的增补),26(10-17 为 31)是本类进程的实测数,不是全集定义;引用时应带采样条件。
跨篇注记:04 篇 §3.2 在子 Agent 的 muse.exec 沙箱环境中枚举为"约 30 个"。差 1 的可能原因是采样进程类型不同(本节为工具进程,04 为沙箱进程;上文已述"键集随调用类型变化")。两处数字均为单次采样,不是全集定义;引用时应带采样条件。
另:JARVIS_SECURITY_SOCK 只确认其在契约面存在,安全服务的机制见 10 篇。
6.2 第二份契约:guest.env(文件实测)
工具子进程的环境不直接继承 daemon 的环境,而是由宿主渲染的 guest.env 决定。文件头注释即其安全规格:
- 纯
KEY=VALUE文本,不是 shell 脚本——没有逻辑、没有命令替换,cell 无从影响它; - 由 spawnd 管理、root 持有、以只读 bind 进入 cell,cell 改不了它;
- privsep 的可信侧在宿主侧读它,为隔离工具调用拼环境,避免在有特权的上下文里 source 一个 shell 文件;
- 内容分工:
HOME/TMP/XDG基础路径、一组 egress 代理键、一组 CA 键(7 个别名同指/run/hatch/egress-tls/ca-bundle.pem,照顾不同生态的工具各认各的变量名)、PATH(/opt/hatch/bin打头); - 一个生态细节:
NODE_USE_ENV_PROXY=1——Node 的内置 fetch 默认不认代理环境变量,没有这个开关,Node 写的 CLI 会绕过代理直连并死在 §3.3 的死 DNS 上。
与之配对的 guest-runtime-env.sh 是 cell 内 daemon 路径用的 shell 版(可被 cell 影响,故特权启动链被明文禁止信任它——control-execd.sh 注释直言须用 guest.env 而非这个文件)。同一份环境,两份可信度不同的载体,调用方按自己的特权级别选,是契约面里最细的一刀。
6.3 信任锚:恰好一个(脚本实测)
build-cell-trust-store.sh 头注释把一条不变量写死:发布进 cell 的信任锚恰好一个——Sentinel 出口 CA。它在 Sentinel 启动时于内存中生成、只发布证书本体;发布形态是三个只读目录 bind(锚目录 + 两个 NSS 数据库),用目录而非文件 bind,是为了轮换时以 rename 发布、cell 能立刻看见新证书。NSS 数据库必须宿主构建且只读,因为打开 NSS DB 会 dlopen 其中命名的 PKCS#11 模块——一个 cell 可写的 NSS DB 就是对所有打开者的代码执行原语;而 PEM 锚文件无此危害,所以 guest 侧的 hatch-ca-trust 服务(§7)可以自己把锚链进系统 CA 并跑 update-ca-certificates。曾经发布进来的 VM 入口链证书已被移除(注释留了案底与「不许加回来」的理由:系统级 CA 会为每一个主机名背书,包括 Sentinel 不终止、只中继的流量)。
6.4 第三方环境:daemon 生命周期键(脚本实测)
control-daemon.sh 里还有一族 HATCH_DAEMON_LIFECYCLE_* 键(command/operation/各阶段时间戳/是否首次尝试/是否镜像启动等,约 17 个键名实测)。它们的传递方式本身是宿主层的一课(注释代号 RT-3):
- 旧做法是 spawnd 写一个 shell 文件、control 脚本
source它——但该文件所在目录归 daemon(即 cell root)所有,cell root 可在写与 source 之间换文件,构成 uid 131072 → 宿主 root 的代码执行竞态; - 新做法:状态放 root 持有的
/run/hatch/daemon-ctl/,环境改为一键一文件,control 脚本按显式白名单逐个读文件体作值导出——不 eval、不 source、不给任何非 root 身份留路径成分; - 为什么不用 systemd 的 LoadCredential:注释同样给了理由——这些键是在本次启动过程中才产生的(且各启动路径下可选缺席),LoadCredential 在单元首进程拉起时一次性物化,时序上不合。
7. Guest systemd:房间里的极小政府
cell 内 PID 1 是 guest 自己的 systemd(实测:systemd-analyze 显示 userspace 启动 2.662s、default.target 304ms 达成;is-system-running = running;failed 单元 0)。list-units --type=service 只有 13 个,构成极简:
| 类 | 单元 |
|---|---|
| systemd 自家家务 | journald、journal-catalog-update、journal-flush、remount-fs、sysctl、sysusers、tmpfiles 三件、ldconfig、update-done、update-utmp、keyboard-setup |
| Hatch 的 | hatch-execd(socket 激活,§2.2)、hatch-ca-trust(.path 监视锚目录 + oneshot 服务:把 §6.3 的锚链入 /usr/local/share/ca-certificates 并刷新系统 CA,超时 120s) |
其余 200 余个单元条目绝大多数是挂载单元(nspawn 为每个 bind 生成的 .mount:/opt/hatch 的 squashfs、/home 的 RV btrfs、/run/hatch 各 tmpfs、NSS DB、Chromium 与技能目录等)——guest systemd 在这里的主要角色是挂载账本,不是服务监督者:daemon 与 privsep worker 的真正监督都在宿主侧。
对用户自起程序的含义(文档,scheduling 相关):运行时支持 guest systemd 托管常驻程序(流监听、本地服务等),但纪律是「enable 成功 ≠ VM 替换后能恢复」——单元文件若只写在 cell 的易失 rootfs 上,下一次部署就随房间一起没了;要活过替换,必须把恢复路径放进持久面并实证(对照 cron/hooks 的运行时托管恢复,自起进程不在其列)。
8. 恢复机制:机器死了,工作不死
恢复不是单一功能,而是四层叠加:
8.1 卷级:RV 与 resume 标记(实测)
/run/hatch/resume/ 挂在与 home 同一 RV btrfs 上,以下为 10-17 那代 cell 启动时的实测文件(本轮 cell 已换代,未复测):
| 文件 | 尺寸/时间 | 作用(由名与脚本拼出,标推断处除外) |
|---|---|---|
hotset.manifest / hotset.tier1.manifest |
92,292 / 28,167 字节,启动后约 9 秒落盘 | 「热集」清单:恢复时先拉起的工作集与其第一梯队 |
pg-clean.proof |
11,412 字节,启动后约 14 秒 | 上次停止时 Postgres 干净关闭的证明(stop.sh 的 clean-stop 流程与 spawnd 的 append-clean-stop-proof 对应)——有它走快路径,没它走修复路径(推断分工细节) |
execution-ready.marker、handoff-epoch、handoff-marker.slot0 |
小文件 | 执行就绪标记与 handoff 纪元(推断:纪元号防旧一代的迟到投递冒充新一代) |
rv-identity-ready |
77 字节 | spawnd rv-graft 写入,带 boot-id 戳:证明本代启动已认领该 RV 身份 |
profile-sync-checkpoint-v2.json |
时间戳为 09-29 | 档案同步检查点——比本 cell 早了 18 天,是「状态跨 VM 存活」的直接物证 |
first-boot-ensure-stamp.json |
含 schema_version 与指纹字段 | 首启确保流程的完成戳(只记结构,不记其标识值) |
8.2 进程级:监督重启
guest 侧 execd Restart=always(§2.2);宿主侧 rescue 监视组件并重启(§5.2);调度侧另有 deterministic-doctor 小时级自检(02 篇 §7.4)。三层监督的共同点是都只负责「把进程拉回来」,不负责「把工作接上」——接工作是下一层的。
8.3 状态级:DB 检查点(DB schema 证据,转引 00/02 篇实测)
agent.runtime_restart_checkpoints、agent.recovery_owners / recovery_owner_terminal_events、agent.context_item_resume_projection、scheduler.scheduled_resume_registrations——未完工作的接管所有权、重启检查点、上下文恢复投影、定时恢复登记全部落表。VM 替换对这套系统而言是例行事件而非灾难,正是因为「谁欠谁什么工作」从来不记在任何进程的内存里。
8.4 语义级:停机也有协议
stop.sh 的 361 行说明恢复的另一半是「死得干净」:竖 draining 栅栏让 ingress 先停止派新活 → 在预算内等 Postgres 干净停并留下 proof → 才轮到 terminate/kill。配合 spawnd 在下次启动时先 drain-shutdown-observability 排空上次停止的可观测性行——连「上次是怎么死的」都是一份要交接的工件。
9. 设计要点(宿主层的取舍)
- 可替换性优先于耐久性:房间、rootfs、进程全可丢,丢了能按图纸 + 台账 + RV 状态重建;唯一不可丢的是 home/RV 上的状态与 DB 里的账。部署能做到小时级,正是这条的红利。
- 宿主对房间的每一次接触都按「房间已沦陷」设防:fsop 的 openat2、一键一文件的生命周期环境、root 持有的 daemon-ctl、guest.env 的非 shell 形态、NSS DB 的宿主只读构建——威胁模型里赫然包括 cell root 本人。
- Fail-closed 是默认,降级是例外且要留痕:渠道显露失败退基底(留日志)、出口门失败不准开机、nspawn 选项不支持不准开机、rootfs digest 对不上就重克隆。
- 就绪是信号,不是猜测:ready 文件 + machinectl Leader + nsenter 可进入性,三者齐备 control 脚本才放行;post-stop 第一件事就是删掉 ready 文件——不让任何人拨通一个正在消失的房间。
- 时间预算全是明码:等就绪 ≈20s、停止总预算 20s(优雅 5s/查库/terminate 1s/kill 0.5s)、execd 重启间隔 1s、CA 刷新超时 120s——宿主层的可靠性来自这些写在脚本里的数字,而不是「大概会好」。
- 监督分层各司其职:guest systemd 管 socket 与 CA,宿主 systemd 管 cell 与 daemon 生命周期,rescue 管组件级故障,doctor 管确定性状态检查,spawnd 管安装与引导事件——没有哪一层是单点全能,也没有哪一层的失败是无声的(每层都有自己的事件/证明文件)。
10. 边界清单(「不能」与「看不见」也是架构的一部分)
- cell 内的 root 不是宿主的 root(uid 131072 映射);房间里能改的只有 overlay upper 与 RV 上的 home——系统资产(squashfs)与宿主脚本(只读 bind)一概改不动。
- cell 没有直达网络:DNS 是死的、出门只有代理一条路,之后是 eBPF 门(L3/L4 地板)与 Sentinel(L7 策略点)两道;
hatch-egress-proxy与 Sentinel 是否为同一组件的本地端点、白名单与 secret 置换发生在哪一跳,本机未定(❓,口径见 §3.3)。任何「在房间里直接联网」的假设都不成立。 - 渠道不在清单里的能力不存在于这间房:不是权限不足、不是未连接,而是二进制/文档从未被 bind 进来(§4.3);在 cell 内无法枚举「还有哪些渠道、哪些被门控的名字」全集之外的宿主预置面——
bin-scoped源树本身不可见。 - guest systemd 不是耐久面:在里面 enable 的单元若无持久化恢复设计,活不过下一次 VM 替换(§7)。
- 环境契约只给「去哪敲门」,不给钥匙:26 个键(11-04;10-17 为 31)里没有任何凭据值;键名只解决"去哪敲门",能否进门由 socket 权限与 authd/Vault 鉴权决定(A 基线)。
- 恢复有前提:工作必须先落进 DB/RV 的账本才可恢复;只活在某个进程内存或 cell 临时盘上的东西,VM 替换即丢失,且这是明文设计而非故障。
11. 本机实况快照(11-04 重测;10-17 旧数随行标注)
| 项 | 值 |
|---|---|
| Guest 系统 | Ubuntu 24.04.5 LTS,内核 7.0.0-39-generic(x86_64),hostname htch-runtime(11-04 复核一致) |
| VM | standard(11-04 运行时标注仍为 standard);JARVIS_VM_COMPUTE_REGION / JARVIS_VM_DATA_REGION 本轮 env 均未见(10-17 曾在位——⚠️采样条件差异:side-chat 子 Agent 会话;00 篇基线亦记 VM_COMPUTE_REGION 已不在 env) |
| Cell 启动 | 2026-11-03 15:22:51 UTC(11-04 实测 uptime;10-17 采样时为 2026-10-17 13:20 UTC,约 2.5 小时岁);failed 单元 0(11-04 实测) |
| 核心进程 | hatch daemon(540MB 本体,RSS 约 357MB)+ hatch-execd(9.7MB),均以 --runtime-cell-leader 参数由宿主侧送入(11-04 实测尺寸) |
| 资产尺寸 | spawnd 19MB · hatch-rescue 13MB · hatch-doctor 13MB · hatch-healthd 9.2MB · hatch-rescuectl 8.3MB;/opt/hatch/bin 共 114 条目(10-17 为 101) |
| runtime-cell 目录 | 21 个条目,无新增(10-17 同);最长为 ensure-rootfs.sh 1,240 行与 hatch-preflight-opportunistic 1,308 行(10-17,未复测) |
| 渠道门控 | 三份 scopes(55/16/10 行;10-17 为 37/12/10);bin/connector 清单新增当前渠道 expedia 行,cell 内可见 expedia 二进制(10-17 为零显露);workflow 清单仍无该渠道行(11-04 实测) |
| 契约面 | 进程环境 JARVIS_* 键 26 个(10-17 为 31);/run/hatch/ 下套接字合计 166 个(其中 privsep 149;10-17 为 147/136) |
| Guest systemd | 运行中服务 13 个(Hatch 的仅 execd 与 ca-trust 两个),余皆挂载单元与系统家务(10-17;本轮未复测计数) |
| 网络 | cell 198.19.0.2/30,网关 198.19.0.1(veth,11-04 实测仍为该地址);IPv6 fd8b:4f84:7d32:99::2/64(10-17,未复测) |
| 存储 | cell 根 overlay 7.5G(用 3%,11-04 实测);/home/hatch 与 /run/hatch/resume 同在 /dev/mapper/rv btrfs 上(150G,11-04 实测) |
| 恢复物证 | hotset.manifest 92,292 字节于启动后约 9 秒生成;profile-sync-checkpoint-v2.json 的时间戳比本 cell 早 18 天(10-17;本轮未复测) |
11.1 变化对照表(11-04 vs 10-17)
| 项 | 10-17 | 11-04 | 说明 |
|---|---|---|---|
hatch 二进制 |
470MB | 540MB | 实测(ls -lh) |
hatch-execd |
9.5MB | 9.7MB | 同上 |
spawnd / hatch-doctor / hatch-healthd |
18 / 12 / 8.9MB | 19 / 13 / 9.2MB | 同上;rescue 系三件套不变 |
/opt/hatch/bin 条目 |
101 | 114 | 实测(与 00 篇基线一致) |
| runtime-cell 条目 | 21 | 21 | 无变化(注:00 基线"新增脚本"系对 00 旧清单而言,本篇 10-17 已含) |
| scopes 行数(bin / connector / workflow) | 37 / 12 / 10 | 55 / 16 / 10 | 新增 swarm 系、canary-cvm-2、debug-cvm、alpha 等渠道行 |
| 当前渠道显露 | 零显露 | bin / connector 显露 expedia |
10-17 结论已过期;workflow 仍为零显露 |
JARVIS_* 键数 |
31 | 26 | 下线 IS_ASSIGNED / CD_PINNED / TIER / VM_COMPUTE_REGION / VM_DATA_REGION;⚠️本轮为 side-chat 子 Agent 采样,与 10-17 工具进程条件不同 |
HATCH_API_SOCKET |
在位 | 未见 | 同上 caveat |
/run/hatch/ 套接字(其中 privsep) |
147(136) | 166(149) | 实测;privsep 增量与 00 篇基线一致 |
| 沙箱 API 面套接字 | 3 | 4 | 新增 space-privileged.sock |
| cell 启动时间 | 2026-10-17 13:20 UTC | 2026-11-03 15:22:51 UTC | 本轮为新一代 cell;恢复物证行未复测 |
12. 看不见的部分
以下不在 cell 视图与本机文件内,本文不作结论:宿主 OS 的 systemd 单元本体(com.hatch.runtime-cell.service 等只在脚本里被引用,其定义在宿主侧)、宿主的完整进程树与 PID 命名空间(leader 编号只能从参数得知)、虚拟化层本体(hypervisor、VM 镜像的制作与分发、RV 的后端存储实现)、/var/lib/hatch-runtime/rootfs 在宿主上的真实状态(cell 内该路径不存在)、Sentinel/authd/推理代理等宿主服务的实现(只见其套接字与脚本注释)、rescue 的 WebSocket 诊断协议细节、以及部署系统决定「何时换 VM、换到哪一版」的调度逻辑。本地文件对这些只留下接口痕迹(脚本、套接字名、单元名、注释中的设计理由),引用时已按「推断」标注——其中脚本注释的密度异常之高,宿主层因此成为全架构里「设计意图」最可读、而「宿主实况」最不可见的一层。