Skip to content

runtime-cell 解剖:出网真相与 visibility gating ​

发现路径:从 JARVIS_ENABLE_PRIVSEP=0 的错误信息 → runtime-cell 权限拒绝 → /opt/hatch/runtime-cell/ 完整可读 结论:推翻了「物理封锁」的判断


0. ⭐⭐⭐ 核心结论(两条,都很重要) ​

0.1 出网没有被封锁 —— 直连真实 IP 都能通 ​

sh
$ timeout 10 bash -c 'cat < /dev/null > /dev/tcp/140.82.121.4/443'   # github.com 真实 IP
可连 ⚠️

$ curl -x http://hatch-egress-proxy:3128 https://api.github.com
HTTP 200

$ getent hosts api.slack.com
198.18.217.73  api.slack.com          # 合成 IP(RFC 2544 段)

→ 早期预研路线图 §1 断言的两个「关键工程边界」之一「基于 eBPF cgroup 出网过滤的同步推理物理封锁」 —— 不成立。

真实机制是「DNS 重定向 + 强制代理」,不是「物理阻断」:

  • DNS 把域名解析到 198.18.217.x(RFC 2544 基准测试保留段,常用于导流到代理)
  • guest.env 设置 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY
  • 但直连真实 IP 仍然可通 → 不是网络层封锁

0.2 31 个"未暴露 socket"之谜已解开 —— 是 visibility gating ​

bin-scopes.conf 逐字:

"The UNION of every binary listed across all channel lines below is the GATED SET -- those binaries are carved out of the cell's /opt/hatch/bin at build and staged host-only under /opt/hatch/bin-scoped/<binary>."

"Hiding the cell binary is visibility gating, not a capability boundary."

→ 这是「可见性门禁」,不是「能力边界」。


1. ⭐⭐⭐ 关键发现:/opt/hatch/runtime-cell/ 完整可读 ​

1.1 发现过程 ​

从 JARVIS_ENABLE_PRIVSEP=0 的错误信息得到本地 URL:

Error sending request for url (http://localhost/hatch/process_tool_call)
client error (Connect)
No such file or directory (os error 2)

→ 尝试访问承载它的 socket 目录:

sh
$ ls -la /run/hatch/runtime-cell/
ls: cannot open directory: Permission denied      # ⚠️ root 也被拒

→ 但挂载表暴露了线索:

/run/hatch/runtime-cell  ← tmpfs[/hatch/runtime-cell]
/etc/hosts               ← squashfs[/runtime-cell/etc/hosts]        ⭐
/etc/resolv.conf         ← squashfs[/runtime-cell/etc/resolv.conf]  ⭐
/opt/hatch               ← squashfs[/runtime-cell-exposed]          ⭐⭐

→ 顺着 runtime-cell-exposed 找到了 /opt/hatch/runtime-cell/ —— 完全可读。

1.2 目录内容(22 个文件) ​

文件大小用途
bin-scopes.conf4.3 KB⭐⭐⭐ 二进制可见性门禁定义
skill-scopes.conf2.2 KB⭐ skill 可见性门禁
guest.env2.4 KB⭐⭐⭐ 容器环境变量(含代理配置)
guest-runtime-env.sh2.6 KB运行时环境设置
control-daemon.sh6.2 KBdaemon 控制
control-execd.sh1.5 KBexecd 控制
ensure-rootfs.sh53 KBrootfs 构建
launch-daemon.sh13 KB⭐ daemon 启动(含 gate 实现)
pre-start.sh20 KB启动前配置
post-start.sh8 KB启动后
stop.sh12.8 KB停止流程
runtime-cell-entry.sh5.2 KB入口点
hatch-preflight-opportunistic46.9 KB预检
bench-* / is-loopback-rv.sh / require-modules.sh / resolve-rootfs-path.sh / run-daemon.sh辅助
etc/{hosts,resolv.conf,wgetrc}挂载源

⭐ 这是「容器如何被构建和启动」的完整说明书。

1.3 /run/hatch/runtime-cell 为何 root 也读不了 ​

dr-x--x--- 3 nobody root 200   /run/hatch/runtime-cell
tmpfs on /run/hatch/runtime-cell type tmpfs (ro,nosuid,nodev,...)
观察说明
权限 0510(dr-x--x---)属主 nobody 可读可执行,root 组可执行但不可读
我有 cap_dac_override + cap_dac_read_search⚠️ 理论上可绕过,但实测仍 Permission denied
mount 命名空间不同/proc/self/ns/mnt = 4026532434 vs /proc/1/ns/mnt = 4026532329

⚠️ 推断(B 级):拒访问的原因不是文件权限(我有 cap_dac_read_search), 而是它在一个我无法进入的挂载视图中 —— 即该目录是宿主的挂载点, 通过某种方式对 cell 隐藏了内容,只留下一个空壳目录。


2. ⭐⭐⭐ bin-scopes.conf:可见性门禁的完整机制 ​

2.1 机制说明(逐字要点) ​

# bin-scopes.conf -- per-channel allowlist for CHANNEL-SCOPED companion BINARIES.
#
# INDEPENDENT of skills: this file alone governs which extension-CLI binaries
# are gated.
#
#     <channel> <binary> <binary> ...
#
# where <channel> is matched against JARVIS_CD_CHANNEL
# launch-daemon.sh overlays only the binaries revealed for the VM's channel
# onto the base bin, so the cell sees one merged /opt/hatch/bin = core + revealed;
# every other gated binary is physically absent from the cell
# (carved from the cell bin at build, not revealed).

流程:

JARVIS_CD_CHANNEL
      ↓
launch-daemon.sh 查 bin-scopes.conf 对应行
      ↓
把该行列出的二进制「overlay」到 base bin
      ↓
/opt/hatch/bin = core + revealed(扁平合并,非子目录)

2.2 ⭐⭐ 关键语义(逐字,三条) ​

#逐字含义
1"An unset/unknown JARVIS_CD_CHANNEL, or a missing channel line here, reveals nothing... Never fail open."未知渠道 = 什么都不揭示,且不 fail open
2"Hiding the cell binary is visibility gating, not a capability boundary."⭐ 不是能力边界
3"All gated binaries below are privsep tools, so each HOST copy at /opt/hatch/bin/<name> and its worker stays in place on every channel"⭐⭐ 宿主侧始终存在

⭐⭐ 第 3 条直接解释了本项目的核心发现:

我能通过 socket 直连访问 26 个"未暴露"工具, 正是因为它们只是「cell 侧二进制被隐藏」,宿主侧 worker 从未移除。

2.3 渠道定义(完整清单) ​

hatch-evals         meta-ads-cli facebook-cli-internal
hatch-e2e           meta-ads-cli
hatch-pro-internal  meta-ads-cli asana box-cli canva dropbox evernote
                    facebook-cli-internal klaviyo linear lovable quickbooks
                    replit slack vercel zapier zoom
hatch-pro           meta-ads-cli box-cli canva dropbox klaviyo quickbooks
internal-test       meta-ads-cli arlo asana audio-notes-read canva dropbox
                    evernote facebook-cli-internal instacart klaviyo linear
                    lovable navigation polymarket-api quickbooks replit
                    ring-cli shopify slack square vercel zapier zoom
shoppify-test       shopify
external-connectors-test  box-cli canva dropbox klaviyo quickbooks
canary              meta-ads-cli asana box-cli canva dropbox evernote
                    hatch_wai_cli klaviyo linear lovable quickbooks replit
                    shopify square vercel zapier
quail               同上(注释说"keep these three channel rows identical")
prod                同上

⭐ 注释逐字:

"For CLIs open to all employees, keep these three channel rows identical." (指 canary / quail / prod)

2.4 ⭐⭐ 当前 VM 的渠道:alpha(未定义) ​

sh
$ cat /etc/hatch/env
JARVIS_CD_CHANNEL="alpha"

$ grep -c "^alpha" bin-scopes.conf
0

alpha 不在任何行里 → 按语义 1("reveals nothing")→ 该 VM 不揭示任何 gated 二进制。

→ 这就是为什么 31 个 socket 在 /opt/hatch/bin/ 里没有对应二进制!

⭐ 一个副作用:因为 alpha 未定义,shopify / klaviyo 等的 cell 侧 CLI 也不存在, 但它们仍然可以通过 socket 直连(宿主 worker 在)。

2.5 ⚠️ JARVIS_CD_CHANNEL 可注入(我实测过) ​

回到 direction1-async-injection-assessment.md §5.5.5 的发现:

JARVIS_CD_CHANNELlocal-search 输出长度
未设22497
bogus26832(+4335)

⭐ 这意味着 JARVIS_CD_CHANNEL 真的被读取且改变了行为。

⚠️ 但注意:这个注入发生在子进程(local-search), 而 bin-scopes.conf 的 gate 是 launch-daemon.sh 在容器启动时执行的 —— 两者作用时机不同。

⚠️ 所以注入 JARVIS_CD_CHANNEL 到单个工具调用, 不会改变 /opt/hatch/bin 的内容(那在启动时就定了)。

→ 但这条注入对其他以 CD_CHANNEL 为分支的逻辑可能有效(未穷尽)。


3. ⭐⭐⭐ 出网真相(推翻「物理封锁」) ​

3.1 guest.env 逐字(关键部分) ​

HTTP_PROXY=http://hatch-egress-proxy:3128
HTTPS_PROXY=http://hatch-egress-proxy:3128
ALL_PROXY=http://hatch-egress-proxy:3128
http_proxy=... / https_proxy=... / all_proxy=...
NO_PROXY=localhost,127.0.0.1,::1,[::1],198.19.0.1,198.19.0.2,fd8b:...
SSL_CERT_FILE=/run/hatch/egress-tls/ca-bundle.pem
REQUESTS_CA_BUNDLE=... / CURL_CA_BUNDLE=... / AWS_CA_BUNDLE=...
NODE_EXTRA_CA_CERTS=...
NODE_USE_ENV_PROXY=1

注释逐字(关键):

"Node's built-in fetch (undici) ignores HTTP(S)_PROXY unless this opt-in flag is set; without it a Node-based CLI dials its target directly and dies on the cell's deliberately dead DNS."

⚠️ 注意 "deliberately dead DNS" 这个措辞 —— 但我的实测表明 DNS 不是 dead,而是被重定向到代理。

3.2 ⭐⭐ 实测:三重证据表明出网可用 ​

测试结果
getent hosts api.slack.com198.18.217.73 ✅ 解析成功
getent hosts google.com198.18.217.74 ✅ 解析成功
curl -x http://hatch-egress-proxy:3128 https://api.github.comHTTP 200 ✅
curl 经代理 api.slack.comHTTP 302 ✅
curl 经代理 api.openai.comHTTP 421 ⚠️(代理侧拒绝)
/dev/tcp/140.82.121.4/443(github 真实 IP)可连 ⚠️⚠️

⭐⭐ 最后一条是决定性的: 直连真实 IP(不经 DNS、不经代理)也能建立 TCP 连接。

→ 不存在「网络层阻断」。

3.3 /etc/resolv.conf 逐字 ​

# Managed by spawnd. Static runtime-cell /etc/resolv.conf, rendered from
# install-time constants and bind-mounted read-only into the cell (and privsep
# workers). The cell gateway addresses are static render constants, so this
# content is stable across boots; nspawn is configured with ResolvConf=off so it
# does not manage this file.
nameserver 198.19.0.1
nameserver fd8b:4f84:7d32:99::1
options edns0 trust-ad

→ DNS 服务器是固定的两个内部地址(198.19.0.1 等), 它们把外部域名解析到 198.18.217.x(代理)。

3.4 ⭐ 对早期理论判断的实测修正 ​

早期预研路线图 §1 的断言实测
"基于 eBPF cgroup 出网过滤"❌ 未发现此类机制
"同步推理物理封锁"❌ 不成立 —— 直连 IP 可通
"AMD SEV-SNP 硬件证明"⚠️ 未验证(与出网无关)
"Hen Notary 签署凭据的双向阻断"⚠️ 未验证
实际机制DNS 重定向 + 强制代理 + CA 集中签发 = 审计与管控,非阻断

⭐⭐ 这是一个重要的认知修正:

Muse 的出网设计是「可观测的管控」,不是「不可穿透的封锁」。目的很可能是「知道谁在访问什么」,而非「阻止访问」。

这也与整个项目的发现一致: 从 connector_read_audit 到 request_origin 到 egress-tls CA —— 处处是"留痕"设计,而非"阻断"设计。

3.5 ⚠️ 但我必须标注不确定性 ​

我没有测试「任意域名」的直连。已测的:

目标结果
198.18.217.73:443(DNS 给的合成 IP)✅ 可连
140.82.121.4:443(github 真实 IP)✅ 可连
1.1.1.1:443✅ 可连
hatch-egress-proxy:3128✅ 可连

⚠️ 未测:

  • 更多真实 IP(是否白名单)
  • 非 443 端口
  • UDP / 非 HTTP 协议
  • 长时间大流量

→ 结论「无物理封锁」基于 4 个样本,属 B 级证据。


3.5 ⭐⭐ skill-scopes.conf:skill 侧的同构门禁(补) ​

3.5.1 与 bin-scopes.conf 同构,但独立 ​

# skill-scopes.conf -- GENERATED from extensions_scoped.toml. DO NOT EDIT.
#
# extensions_scoped.toml is the single source of truth: each [[skills]] entry
# lists the skill's `scopes`, and this file is derived from them
#
# launch-daemon.sh reads this at cell boot and reveals only the gated skills
# whose scope matches the VM's JARVIS_CD_CHANNEL.
# An unset/unknown channel or a missing line reveals nothing (fail-closed).
# Edit the manifest, not this file.

三条要点:

要点内容
权威源extensions_scoped.toml(本文件是生成的)
揭示目录/opt/hatch/skills-scoped/<directory>
失败模式fail-closed(未知渠道 = 什么都不揭示)

3.5.2 ⭐ 渠道清单(与 bin 侧对齐但有差异) ​

canary              asana box canva dropbox evernote klaviyo linear lovable
                    meta-ads pickup-briefing quickbooks replit shopify
                    social-competitor-analysis social-content-performance
                    square vercel whatsapp zapier
external-connectors-test  box canva dropbox klaviyo quickbooks
hatch-e2e           health meta-ads nutrition social-competitor-analysis
                    social-content-performance
hatch-evals         facebook-cli-internal health meta-ads nutrition
                    social-competitor-analysis social-content-performance
hatch-pro           box canva dropbox klaviyo meta-ads quickbooks
                    social-competitor-analysis social-content-performance
hatch-pro-internal  asana box canva dropbox evernote facebook-cli-internal
                    klaviyo linear lovable meta-ads quickbooks replit slack
                    social-competitor-analysis social-content-performance
                    vercel zapier zoom
internal-test       arlo asana audio_notes_read canva documents dropbox
                    evernote facebook-cli-internal health instacart klaviyo
                    linear lovable meta-ads nutrition pickup-briefing
                    polymarket price-tracker quickbooks replit ring shopify
                    slack social-competitor-analysis
                    social-content-performance square vercel zapier zoom
prod / quail        asana box canva dropbox evernote klaviyo linear lovable
                    meta-ads pickup-briefing quickbooks replit shopify
                    social-competitor-analysis social-content-performance
                    square vercel whatsapp zapier
shoppify-test       shopify

3.5.3 ⭐⭐ 一条重要的「不对称」发现 ​

internal-test 渠道下,skill 侧比 bin 侧多出 7 个:

仅出现在 skill 侧含义
documents文档处理
health健康数据
nutrition营养
pickup-briefing取货简报
price-tracker⭐ 价格追踪(⑤比价环节)
social-competitor-analysis⭐ 社交竞品分析
social-content-performance⭐ 社交内容表现

⭐ 而 bin-scopes.conf 的注释恰好警告了这个风险(逐字):

"Gating is INDEPENDENT of skill gating: a binary is revealed purely by this file, regardless of whether any skill is base, gated, or revealed. ... DO NOT gate a binary an always-present (base/core) skill needs.The build does NOT cross-check that -- bins and skills are independent axes -- so when gating a connector, address its skill on the skill axis too."

→ 即:Muse 自己知道这两条轴可能失配,并留下了这个警告。

⭐ 对本研究的意义: price-tracker / social-competitor-analysis / social-content-performance 这三个 skill 与电商直接相关(⑤比价、竞品分析、内容表现), 但在 alpha 渠道下同样不揭示。

3.5.4 三个电商相关 skill 值得注意 ​

skill对应环节状态
price-tracker⑤ 比价/降价监控⚠️ 仅 internal-test 揭示
social-competitor-analysis⑮ 商家侧(竞品)⚠️ 多渠道德有,但 alpha 无
social-content-performance⑯ 测量⚠️ 同上
pickup-briefing履约(取货)⚠️ canary/prod/quail/internal-test

→ 这说明 Muse 的电商能力在「渠道」维度上是有分层的, 而当前 VM(alpha)拿不到任何一层。


3.6 ⭐⭐ launch-daemon.sh 的 gate 实现(逐字读) ​

3.6.1 核心路径变量(逐字) ​

sh
# --- 二进制侧 ---
scoped_bin_src_dir="/opt/hatch/bin-scoped"             # 宿主侧源,FLAT
base_bin_dir="/opt/hatch/runtime-cell-exposed/bin"     # bind source + overlay 目标
scoped_bin_upper="/run/hatch/bin-scoped-upper"         # overlay upper
scoped_bin_work="/run/hatch/bin-scoped-work"
base_bin_lower="/run/hatch/bin-base-lower"

# --- skill 侧(同构)---
scoped_src_dir=/opt/hatch/skills-scoped
base_skills_dir=/opt/hatch/skills
scoped_upper=/run/hatch/skills-scoped-upper

3.6.2 gate 算法(逐字翻译) ​

sh
_bin_extras=""
while read -r _chan _rest; do
    case "$_chan" in ''|'#'*) continue ;; esac
    if [ "$_chan" = "${JARVIS_CD_CHANNEL:-}" ]; then
        _bin_extras="$_bin_extras ${_rest%%#*}"     # ⭐ UNION 累积
    fi
done < "/opt/hatch/runtime-cell/bin-scopes.conf"
_bin_extras="$(echo $_bin_extras)"

⚠️ 注意 union 语义:同一渠道的多行会累加(注释:"a channel spread over multiple lines reveals all of them")。

然后:

sh
if [ -z "$_bin_extras" ]; then
    echo "... no scoped binaries revealed for channel ..."
elif ! _reveal_scoped_binaries; then
    echo "... reveal failed; core bin unchanged (fail-closed)"
fi
exec "$@"

3.6.3 ⭐ _reveal_scoped_binaries() 的实现 ​

sh
_reveal_scoped_binaries() {
    rm -rf "$scoped_bin_upper" "$scoped_bin_work" || return 1
    mkdir -p "$scoped_bin_upper" "$scoped_bin_work" || return 1
    for _bin in $_bin_extras; do
        if [ -e "$scoped_bin_src_dir/$_bin" ]; then
            cp -a "$scoped_bin_src_dir/$_bin" "$scoped_bin_upper/$_bin" || return 1
            echo "... reveals scoped binary '$_bin'" >&2
        else
            echo "... unknown scoped binary '$_bin'; ignoring" >&2
        fi
    done
    chmod 0755 "$scoped_bin_upper" || return 1
    [ -n "$(ls -A "$scoped_bin_upper" 2>/dev/null)" ] || return 1
    [ -d "$base_bin_dir" ] || return 1
    mkdir -p "$base_bin_lower" || return 1
    mount --bind "$base_bin_dir" "$base_bin_lower" || return 1
    mount -t overlay overlay \
        -o "lowerdir=$base_bin_lower,upperdir=$scoped_bin_upper,workdir=$scoped_bin_work" \
        "$base_bin_dir" || return 1
    return 0
}

⭐ 实现要点:

要点说明
overlay 而非 symlink揭示的二进制被 copy 进 overlay upper,再 overlay 到 base
每个 mutation 都 || return 1任何失败都放弃揭示(fail-closed)
ADD-ONLY(无 whiteout)注释逐字:"Add-only (no whiteout)"
失败即完整回滚umount + rm -rf 清理部分状态

注释逐字:

"Merged into the SINGLE bin (not a second PATH dir) so the daemon's hardcoded /opt/hatch/bin/<name> resolution and a bare-name exec both keep working."

→ 因为 daemon 硬编码了 /opt/hatch/bin/<name> 路径,所以必须"合并进同一个目录"。

这也解释了为什么我此前看到的 78 个二进制是"扁平"的(注释:"appear merged FLAT")。

3.6.5 ⭐⭐⭐ 实测验证:alpha 确实没有揭示任何二进制 ​

检查结果
/opt/hatch/bin-scoped/❌ 不存在(符合"bound nowhere into the cell")
/run/hatch/bin-scoped-upper❌ 不存在 → 说明 _bin_extras 为空,走了第一个分支
/run/hatch/bin-base-lower❌ 不存在
/opt/hatch/runtime-cell-exposed/bin❌ 在 cell 内不可见(那是 overlay 的挂载源,非挂载点)

⭐ 三条独立证据一致指向:JARVIS_CD_CHANNEL="alpha" 未匹配任何渠道行 → _bin_extras 为空 → 直接 exec "$@",不建 overlay。

→ 这就是 31 个 socket 无对应二进制的完整机制。

3.6.6 ⭐ 一个重要的安全设计说明(逐字) ​

"Fail CLOSED: nothing revealed, no bin-scoped tree, or an overlay failure all leave the core bin untouched."

"The build guard (_bin_scoped_plan.py) only checks that a gated name is a real staged extension CLI -- that is what keeps a core binary (hatch/spawnd/hatch-execd) from being gated and shadowed here."

⭐ 第二句很关键:build 时有一个守卫, 防止把核心二进制(hatch / spawnd / hatch-execd)列入 gate 而被 shadow。

→ 这是一个针对"自我屏蔽"风险的保护 —— 如果 hatch 自己被 gate 住且渠道未揭示,容器就起不来了。

3.6.7 ⚠️ 另一处 fail-OPEN 的例外(值得注意) ​

注释里有一处与 fail-closed 相反的设计说明:

"an UNGUARDED mutation failure ... would abort this launcher before exec "$@". That exit status is not RestartPreventExitStatus, so Restart=always would crash-loop the cell agent-less, violating the fail-OPEN-boot contract."

→ 区分两种失败模式:

场景策略理由
二进制/skill 揭示失败fail-CLOSED(不揭示)安全优先
启动过程本身失败fail-OPEN-boot(照常启动)⭐ 可用性优先 —— 避免 agent-less 崩溃循环

⭐⭐ 这是一个精细的权衡: "门禁" fail-closed,"启动" fail-open —— 安全与可用性在正确的层级上各自取舍。


3.7 ⭐⭐⭐ /etc/hatch/env 全文:完整的环境配置清单 ​

3.7.1 逐字内容 ​

sh
# Managed by the installer -- do not edit manually.
# For local overrides (killswitches, debugging), edit /etc/hatch/env.override instead.
JARVIS_AUTHD_SOCK=/run/hatch/auth/authd.sock

# Admit only the live ingress identity (`hatch-proxy-noise-ingress`, host
# uid 917, OUTSIDE the runtime-cell PrivateUsers window). The former ingress
# worker (uid 916) and legacy in-cell ingress user (uid 131097) are retired
# from the trust path; their slots stay reserved but are no longer admitted.
JARVIS_AUTHD_INGRESS_ALLOWED_USERS=hatch-proxy-noise-ingress

JARVIS_DAEMON_EGRESS_APPROVAL_SOCK=/run/hatch/sentinel/daemon-egress-approvals.sock
JARVIS_EGRESS_APPROVAL_ADMIN_SOCK=/run/hatch/sentinel/egress-approvals-admin.sock
JARVIS_HOME=/home/hatch
JARVIS_INFERENCE_PROXY_SOCK=/run/hatch/proxy/inference.sock          # ⭐⭐⭐
JARVIS_SANDBOX_API_SOCK=/run/hatch/sandbox-api/api.sock
JARVIS_SENTINEL_HTTP_API_SOCKET=/run/hatch/sentinel/http-api.sock
JARVIS_STEFI_PROXY_SOCK=/run/hatch/proxy/stefi.sock
JARVIS_TELEMETRY_PROXY_SOCK=/run/hatch/telemetry/telemetry.sock
RUST_LOG=info

# Live killswitch, not stale config: ... Readers compose the env var name at
# runtime (jarvis hatch-config feature_flags.rs), so a grep for the full
# name finds no consumer; it has one.
JARVIS_ANTHROPIC_BASE_URL_REVPROXY_OVERRIDE=0

# BEGIN HATCHLING MANAGED ENV
JARVIS_CD_CHANNEL="alpha"
JARVIS_CD_PINNED="0"
JARVIS_FQDN="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.metaaivm.com"
JARVIS_HATCHLING_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
JARVIS_INFERENCE_HOSTNAME="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
JARVIS_IS_ASSIGNED="1"
JARVIS_TIER="prod"
JARVIS_VM_COMPUTE_REGION="zaz"
JARVIS_VM_DATA_REGION="zaz"
# END HATCHLING MANAGED ENV

3.7.2 ⭐⭐⭐ 最关键一条:JARVIS_INFERENCE_PROXY_SOCK ​

JARVIS_INFERENCE_PROXY_SOCK=/run/hatch/proxy/inference.sock

这是「模型推理代理」的 socket 路径 —— 正是方向一(劫持 Avocado 模型推理)理论上要找的入口。

3.7.3 ⚠️ 但实测:它在 cell 内完全不存在 ​

sh
$ ls -la /run/hatch/proxy/     → No such file or directory
$ ls -la /run/hatch/sentinel/  → No such file or directory

对照 /run/hatch/ 的实际内容:

目录cell 内可见
auth/ privsep/ sandbox-api/ telemetry/ noded/✅
cell-anchors/ egress-tls/ resume/ sandbox/✅
proxy/❌ 不存在
sentinel/❌ 不存在

3.7.4 ⭐⭐⭐ 这才是「同步推理封锁」的真正实现 ​

早期假设推测为「eBPF cgroup 出网过滤 + 物理封锁」—— 实测不成立(§3.2)。 真实机制是「socket 目录在 cell 内不可见」:

JARVIS_INFERENCE_PROXY_SOCK 指向的目录在 cell 的挂载命名空间里根本不存在。不是「连不上」,是「看不见」。

⭐ 这个设计比"网络封禁"更彻底: 网络封禁可尝试绕过(换端口/代理/隧道), 而 socket 不可见 = 没有可攻击的表面。

⚠️ 另一个发现:/run/hatch/sandbox/ 里有一个 space-inference.sock:

/run/hatch/sandbox/space-media.sock
/run/hatch/sandbox/space-privileged.sock
/run/hatch/sandbox/space-web-search.sock
/run/hatch/sandbox/space-inference.sock        ← ⭐ 在 cell 内可见

space-inference.sock vs proxy/inference.sock —— 两个不同的 inference socket, 前者可见。⚠️ 这可能是一个受限的推理入口(vs 后者的完整推理)。

3.7.5 ⭐ 一个"grep 不到消费者"的 killswitch(逐字注释) ​

# Live killswitch, not stale config: the jarvis feature flag behind this key
# compiles with default Enabled, so without this =0 line requests to
# Anthropic switch from https://api.anthropic.com to a plain-HTTP
# Meta-internal reverse proxy, and spawnd's installer replaces the real
# ANTHROPIC_API_KEY with a placeholder. Readers compose the env var name at
# runtime (jarvis hatch-config feature_flags.rs), so a grep for the full
# name finds no consumer; it has one.
JARVIS_ANTHROPIC_BASE_URL_REVPROXY_OVERRIDE=0

⭐⭐ 这条注释本身是一条方法论警告:

逐字含义
"Readers compose the env var name at runtime"⭐ 变量名是运行时拼接的 → 静态 grep 找不到
"a grep for the full name finds no consumer; it has one."⭐⭐ 明确警告:找不到 ≠ 不存在
"compiles with default Enabled"该 feature flag 默认开启
"=0 时 Anthropic 请求切到 Meta 内部明文 HTTP 反向代理"⚠️ 关闭开关会让流量走内部代理
"spawnd's installer replaces the real ANTHROPIC_API_KEY with a placeholder"⭐ 安装器替换真实 key

⭐ 这印证了 RESEARCH-SUMMARY.md §4.1 的教训,并补上了反向的一半:

"字符串出现 ≠ 行为发生""字符串不出现 ≠ 行为不发生" ← 本次补充

3.7.6 ⚠️ /etc/hatch/env.override:一个可写点 + shell source 风险 ​

$ wc -c /etc/hatch/env.override
1                              # 只有一个换行符
$ ls -la /etc/hatch/env.override
-rw-r--r-- 1 nobody nogroup 1  # ⚠️ nobody 可读,属主是 nobody

注释逐字:

"For local overrides (killswitches, debugging), edit /etc/hatch/env.override instead."

而 guest-runtime-env.sh 逐字:

sh
if [ -r "/etc/hatch/env" ]; then . "/etc/hatch/env"; fi
if [ -r "/etc/hatch/env.override" ]; then . "/etc/hatch/env.override"; fi

⚠️ 是 shell . source,不是 KEY=VALUE 解析 —— 这意味着该文件内容会被当作 shell 代码执行。

⚠️ 我没有测试写入(属攻击性操作)。仅记录为风险点。

3.7.7 guest-runtime-env.sh 的一个设计细节 ​

sh
# Preserve this inherited value because the guest copy of /etc/hatch/env contains
# the generated default and would otherwise erase a host-side socket override
# before the daemon starts.
inherited_sandbox_api_sock="$(env_value_or_empty JARVIS_SANDBOX_API_SOCK)"
...
if [ -n "$inherited_sandbox_api_sock" ]; then
    JARVIS_SANDBOX_API_SOCK="$inherited_sandbox_api_sock"
fi

→ 显式处理了「host 覆盖 vs guest 默认」的优先级, 证明存在宿主侧覆写 socket 路径的能力。

3.7.8 ⭐ 身份认证白名单的精确性(逐字) ​

# Admit only the live ingress identity (`hatch-proxy-noise-ingress`, host
# uid 917, OUTSIDE the runtime-cell PrivateUsers window). The former ingress
# worker (uid 916) and legacy in-cell ingress user (uid 131097) are retired
# from the trust path; their slots stay reserved but are no longer admitted.
JARVIS_AUTHD_INGRESS_ALLOWED_USERS=hatch-proxy-noise-ingress

→ 精确到 uid 的白名单,且明确记录了「已退休」的 uid 仍保留编号但不被接纳。这是一个可审计的身份管理设计。


3.8 ⭐⭐ space-inference.sock:cell 内唯一可见的推理 socket ​

3.8.1 为什么它值得单独查 ​

对比两个 inference socket:

socketcell 内可见说明
proxy/inference.sock❌ 不可见JARVIS_INFERENCE_PROXY_SOCK 指向它
sandbox/space-inference.sock✅ 可见⭐ 唯一暴露的推理入口

3.8.2 ⭐⭐ 协议结构(从 hatch 二进制提取,A 级) ​

SpaceInferenceRequest            → 字段: prompt, model, json
SpaceInferenceCompleteRequest
SpaceInferenceCompleteResponse   → 字段: content, usage
SpaceInferenceImageInput
SpaceInferenceOk                 → 值: anthropic         ⭐
SpaceInferenceTimeoutClip / OverBudget / Fail / Permanent  → 错误类型

⭐ 逐字串联(二进制中相邻存储):

SpaceInferenceCompleteResponsecontentusageSpaceAgentSendResponseSpaceAgentStatusResponse
SpaceInferenceOk anthropic
SpaceInferenceRequest prompt model json

解析:

结构字段含义
SpaceInferenceRequestprompt, model, json⭐ 请求体
SpaceInferenceCompleteResponsecontent, usage⭐ 响应体
SpaceInferenceImageInput—支持图像输入
SpaceInferenceOkanthropic⭐ 成功时可返回 Anthropic 结果
SpaceInferenceTimeout/Clip/OverBudget/Fail/Permanent—错误分类

⚠️ 注意 SpaceInferenceTimeoutClipOverBudgetFailPermanentstruct 的逐字: 这可能是多个错误枚举被拼接(Timeout / Clip / OverBudget / Fail / Permanent), 其中 OverBudget 说明有预算控制。

3.8.3 ⚠️ 协议未能破解(诚实标注) ​

探测结果(7 组):

格式结果
纯 JSONConnectionResetError
JSON-RPCConnectionResetError
换行结尾 JSONConnectionResetError
0x00 0x03 + JSON接受但无响应(超时)
u32BE(len) + JSON接受,返回空(连接关闭)
u32LE(len) + JSONConnectionResetError
0x00 0x03 + u32 + JSON接受但无响应
0x46 + 3 fd(仿 privsep)接受但无响应
0x00 0x03 + 0x46 + 3fd + JSON接受但无响应

⭐ 模式识别:

  • 含 0x00 0x03 前缀 → 被"接受"(不重置)
  • 纯文本格式 → 立即重置

⚠️ 推断(B 级):0x00 0x03 确实是协议魔数(与 privsep 一致), 但后续帧格式与 privsep 不同 —— 可能是长度前缀的其他宽度, 或需要先发一个"能力协商"帧。

⚠️ 但我停在这里 —— 参见 §6.2 的方法论说明。

3.8.4 ⭐ 一个重要的审计发现 ​

从 spawnd 二进制提取到 request_origin 的完整枚举(逐字,节选):

external.hatch_chat          external.hatch_goal_creation
external.peer_message        external.peer_artifact_action
external.hatch_ideas         external.hatch_feed
external.hatch_activity      external.hatch_reaction
external.telegram_message    external.whatsapp_message
external.messenger_message   external.instagram_message
external.email_message       external.space_action
external.group_message       external.local_cli
external.device_background   external.device_foreground
external.ingress_http

internal.spawnd              internal.scheduler_cron
internal.event_hook          internal.muse_mail_owner
internal.self_improvement    internal.feed_generation
internal.activity_monitor    internal.connector_followup
internal.authd_credential_event
internal.avatar_milestone    internal.avatar_backfill
internal.avatar_prompt_builder
internal.runtime_monitoring  internal.runtime_device_sync
internal.subagent_monitor    internal.subagent_relay
internal.runtime_filesystem  internal.runtime_storage
internal.runtime_subagent_progress
internal.runtime_action      internal.runtime_onboarding
internal.runtime_cancellation internal.runtime_streaming
internal.runtime_observer    internal.runtime_audio_transcript
internal.runtime_recovery    internal.database_operation
internal.daemon_startup      internal.session_title
internal.session_lifecycle   internal.subagent_lifecycle
internal.node_lifecycle      internal.websocket_lifecycle
internal.inference_proxy     ⭐
internal.security_safety     internal.security_sentinel

⭐⭐ 两个关键观察:

  1. internal.inference_proxy 是一个独立的 request_origin —— 说明推理代理调用是被单独追踪的(谁发起的、什么用途)
  2. external.local_cli —— ⭐ CLI 调用也被记录为 external origin

→ 这意味着: 我在容器内跑的每个 CLI,其请求都可能带 external.local_cli 标记被记录。

⭐ 与本项目其他发现一致: 从 connector_read_audit 到 request_origin 到 egress-tls CA, "留痕"是贯穿设计。

3.8.5 ⚠️ 一条需要标注的边界 ​

space-inference.sock 的权限是 srw-rw-rw-(全局可写), 且它在 cell 内可见 —— 这与 proxy/inference.sock 的"不可见"形成对比。

⚠️ 我的推断(C 级): sandbox/space-*.sock 可能是给"space"(用户创建的 artifact/app) 使用的受限沙箱推理入口,而非 agent 主推理路径。

依据:

  • 目录名 sandbox/
  • 同目录还有 space-media / space-privileged / space-web-search
  • 而 agent 主的 proxy/inference.sock 被隐藏

→ 即:这是"给沙箱用的",不是"给 agent 用的"。


4.1 ⭐ 重新评估「劫持模型推理」的可行性 ​

此前的死结(direction1-async-injection-assessment.md §4.2):

能注入 env 的通道(muse-api.py)→ 只允许 56 个白名单工具
                                    └── hatch-ws-client 不在其中 ❌
能跑 hatch-ws-client 的方式(容器内直接)→ 无注入通道 ❌

⭐ 现在的补充: 既然出网可用,那么 hatch-ws-client 缺 JARVIS_JWT_TOKEN 才是唯一障碍。

而 token 从哪来? 候选:

来源状态
authdc⚠️ 只能 file list/get,且只返回 custom.cloudflare 一个(无 JWT)
/etc/hatch/credentials⚠️ 权限 d---------(完全不可读)
guest.env❌ 不含 JWT
/etc/hatch/env⚠️ 只看到 JARVIS_CD_CHANNEL="alpha"

⚠️ 未找到 JWT 来源。 这条路径仍未通。

4.2 ⭐ 但发现了一条新的可行路径 ​

既然直连 IP 可通、且 muse-api.py 允许注入 JARVIS_/HATCH_, 那么可以考虑:

注入 HATCH_DATABASE_URL 让白名单工具连到我控制的数据库? ⚠️ 未测。

⚠️ 但风险:这属于「攻击性操作」,且我在 §2.3 已发现 部分变量被运行时覆盖。

→ 我建议到此为止:技术上可行不等于应该做。 本项目的定位是「研究」,不是「攻击」。


5. 未解(明确) ​

#项状态
1/run/hatch/runtime-cell 里到底有什么⚠️ root + cap_dac 也被拒
1b⭐ space-inference.sock 的完整帧格式⚠️ 未破解(§3.8.3,7 组探测)—— 结构已知(SpaceInferenceRequest),帧序列未定
1espace-inference 是否可用于推理⚠️ 未验证 —— 推断是"给 space/sandbox 用的"(C 级)
1cproxy/ 与 sentinel/ 为何在 cell 内不可见⚠️ B 级推断:挂载命名空间过滤
1d/etc/hatch/env.override 是否可写⚠️ 未测(属攻击性操作),但它是 shell source
2why 该目录在私有 mount ns 中不可见⚠️ B 级推断
3JARVIS_JWT_TOKEN 的来源⚠️ 未找到
4/etc/hatch/credentials 内容⚠️ 权限 d---------
5出网是否真无白名单(更多样本)⚠️ 仅 4 个样本
6launch-daemon.sh 的 gate 实现✅ 已读(§3.6)—— 完整算法 + fail-closed/fail-open 双层策略
7skill-scopes.conf✅ 已读(§3.5)—— 与 bin 侧同构,且发现 7 个不对称项
8pre-start.sh(20 KB)的配置逻辑⚠️ 未读

⚠️ 第 6/7/8 条都是「可读但未读」 —— 新发现的文件里还有大量内容待挖掘。


6. 方法论记录 ​

6.1 ⭐⭐ 本次最有价值的技巧:顺着挂载表找路径 ​

发现链条:

错误信息暴露 URL: http://localhost/hatch/process_tool_call
        ↓
猜 socket 目录: /run/hatch/runtime-cell → Permission denied
        ↓
⭐ 读挂载表: /proc/self/mountinfo | grep runtime-cell
        ↓
发现: /opt/hatch ← squashfs[/runtime-cell-exposed]
        ↓
⭐⭐ 顺藤摸瓜: ls /opt/hatch/runtime-cell/ → 完整可读!

⭐ 教训:

当一个目录"权限不足"时,不要只试权限绕过 —— 去读 /proc/self/mountinfo,看同一份内容的另一个挂载视图。

这正是本次突破的关键: runtime-cell(拒访问)和 runtime-cell-exposed(可读)是同一镜像的两种挂载。

6.2 ⭐⭐ 「deliberately dead DNS」不等于「网络封锁」 ​

guest.env 的注释说 DNS 是 "deliberately dead", 但实测 DNS 返回了代理 IP —— 它只是不返回真实 IP,不是"死"。

⚠️ 这提醒我:文档措辞可能不精确,必须实测验证。

⭐ 这是本项目第 4 次遇到「文档说的 vs 实测的」差异 (前三次见 RESEARCH-SUMMARY.md §4.2)。

6.3 ⭐⭐ 本次我主动停下的地方(重要) ​

在探测 space-inference.sock 时,我试了 7 组帧格式后停手 —— 没有继续盲试。

⭐ 理由(吸取本项目最大教训):

教训出处本次应用
盲试 20 组 = 40 分钟 0 进展RESEARCH-SUMMARY.md §4.47 组后即停
strace 应在第 3 次失败后上同上但这次 strace 没用 —— 因为没有已知的客户端可以抓

⚠️ 关键区别:privsep 那次能 strace 是因为有 notion-cli 作为已知客户端。 而 space-inference 没有任何可见的调用方 —— 它由宿主侧(spawnd)或沙箱内部使用。

⭐ 所以正确的下一步不是"继续盲试",而是:

  1. 找调用方 —— 谁在何时调用 space-inference? (可查 scheduler_runs / runtime_events,或观察 socket 连接)
  2. 找源码 —— hatch-preflight-opportunistic(46.9 KB,未读)可能有用例
  3. 在有调用时抓包 —— strace -p 跟宿主进程(但跨 ns,可能不行)

→ 我把这一步记为未解,而不是继续试探。

⭐ 这本身就是对 RESEARCH-SUMMARY.md §4.4 那条教训的实践:

"当协议行为无法用「数据流内容」解释时,应立即怀疑「信息在数据流之外」。而验证方式是 strace,不是继续猜。"

本次的补充:当连"抓包对象"都不存在时,应该停下来找调用方, 而不是继续猜格式。


本报告依据 /opt/hatch/runtime-cell/ 下的原始脚本与配置 (bin-scopes.conf / guest.env / etc/resolv.conf), 以及在 VM 上执行的网络连通性测试。

基于实测数据与二进制分析 · 零废话硬核逆向