diff --git a/docs/architecture/2026-08-25-network-egress-seam.md b/docs/architecture/2026-08-25-network-egress-seam.md index 2cef85a6419c..304c37e58ca0 100644 --- a/docs/architecture/2026-08-25-network-egress-seam.md +++ b/docs/architecture/2026-08-25-network-egress-seam.md @@ -180,8 +180,11 @@ https://1.1.1.1/ → 301 ← 纯 raw-IP,无 DNS 也通 > 不是 `models.dev`;②平台族已迁域,注册表从 `shared/alpha-config.ts` 的 `ALPHA_ENDPOINTS` 派生 > (web / platform / account / cloud 四条),不自持域名;③LSP 静态枚举多出 `www.eclipse.org:443` > (`lsp/server.ts:1207`)。动态类别(BYOK baseURL、远程 MCP URL)与 ssh `*:22` **不在**初值里; -> `#1334` Q4 拍到的 `release-assets.githubusercontent.com` 与 Q3 语料的 `example.com` 也不在 —— -> 加不加归 `#1073` 裁。判据:`network-egress-registry.test.ts`(独立字面量锚)。 +> `#1334` Q4 拍到的 `release-assets.githubusercontent.com` 与 Q3 语料的 `example.com` 当时也不在,交 `#1073` 裁。 +> **裁决(owner 2026-09-10,落在 `#1337`)**:前者**加进**注册表(真实开发流量,出货复跑再次实拍到它被 403); +> 后者**不加**(探针靶子,不是开发流量 —— AC3 语料改用注册表里已有的 `registry.npmjs.org`)。 +> 判据:`network-egress-registry.test.ts`(独立字面量锚)。强制层的接线(profile 网络行 + main 进程内代理 + sidecar env) +> 在 [`2026-09-10-network-egress-on-process-fence.md`](2026-09-10-network-egress-on-process-fence.md) §落地。 ## 3. 咽喉点在不在(勘破 3) diff --git a/docs/architecture/2026-09-09-req159-process-fence.md b/docs/architecture/2026-09-09-req159-process-fence.md index cefb762e70cb..a81591c96220 100644 --- a/docs/architecture/2026-09-09-req159-process-fence.md +++ b/docs/architecture/2026-09-09-req159-process-fence.md @@ -127,11 +127,13 @@ U1 只出了 arm64,并明写将来出 Intel / universal 时若漏第二份,x64 那里还记着一条给下一个打包验证的人的坑:**给 app 设 `HOME` 隔离不了它** —— Electron 的 appData 不看 `$HOME`,而 `preferAppEnv` 会用登录 shell env 缓存把 `HOME` 改回去。 - **围栏下没有发过一次真的模型请求**(与四轮勘破同一条)。 -- **网络轴另算,而且老裁决对不上这个接缝**:本围栏今天是 `(allow default)` 的网络面。往它上面加 - 「只连授权目的地」的重勘破在 [`2026-09-10-network-egress-on-process-fence.md`](2026-09-10-network-egress-on-process-fence.md) - (`#1334`)—— 关键一条:2026-08-25 勘破 §5 写下的那个 profile 形状照抄过来会让**引擎起不来** +- **网络轴已落地(REQ-137,`#1336` / `#1337`)**:本围栏的网络面不再是 `(allow default)` —— profile 尾部多了 + `#1334` Q1.2 那四行(`(deny network*)` + loopback bind / inbound + 只放行 main 进程内策略代理的端口), + sidecar 的代理 env 由 main 整份改写指向它。形状、判据与覆盖面声明在 + [`2026-09-10-network-egress-on-process-fence.md`](2026-09-10-network-egress-on-process-fence.md) §落地。 + 那轮勘破的关键一条仍值得记着:2026-08-25 勘破 §5 写下的形状照抄过来会让**引擎起不来** (sidecar 是 HTTP 服务器,`(deny network*)` 连 `listen()` 一起拦),而围栏自己会照常报 - `process fence applied`,失败在今天是哑的。 + `process fence applied` —— 所以 N3(inbound)那一行由 `process-fence-profile.test.ts` 逐字守着。 - **x86_64 片没有被执行过**(§5)。 - **公证 / staple 没做**(U1 §4 同一条;发版 runbook §1 ③ 已要求核对)。 - **`~/.bash_history`** 不在可写集(§2);终端配置降级的披露面已落地(§8,`#1322`),但它只是**告知**, diff --git a/docs/architecture/2026-09-10-network-egress-on-process-fence.md b/docs/architecture/2026-09-10-network-egress-on-process-fence.md index 4e8014895d7f..01455219333f 100644 --- a/docs/architecture/2026-09-10-network-egress-on-process-fence.md +++ b/docs/architecture/2026-09-10-network-egress-on-process-fence.md @@ -306,6 +306,34 @@ process fence profile does not compile even with the minimum writable set 按《时序门需 ≥3 轮采样》**不构成结论**。 - 公证 / staple;x86_64 那一片没有被执行过;单机单配置单拓扑。 +## 落地(`#1337`,2026-09-10) + +上面的读数在 `#1337` 里变成了代码。落地形状,每一行都能在树上指到: + +| 层 | 文件 | 事实 | +| --- | --- | --- | +| 强制层(profile) | `packages/ui-mac/src/main/process-fence-profile.ts` N1–N4 | 可写集之后追加 §Q1.2 那四行**逐字**:`(deny network*)` + loopback `network-bind` / `network-inbound` + 只放行 `localhost:<代理端口>` 出网。只写加法;DNS 刻意不放行。端口不是 1..65535 的整数 ⇒ 拒绝渲染 | +| 策略层(代理) | `packages/ui-mac/src/main/server.ts` `ensureEgressPolicyProxy` → `network-egress-proxy.ts` | 代理跑在 **Electron main 进程内**(围栏外,`#1073` 裁决三:`(deny network*)` 连带拦 DNS,解析只能在围栏外做),第一次 fork 之前起、跨 respawn 复用;起不来 ⇒ 拒 fork。授权只问 `network-egress-registry.ts` | +| 汇流(env) | `sidecar-env.ts` `sidecarEgressProxyEnv` | fork 之前把 sidecar 的八个代理变量**整份改写**:`HTTP(S)_PROXY` / `ALL_PROXY`(大小写)指向代理,`NO_PROXY` 只留 loopback —— 用户自己的代理与 NO_PROXY 名单不存活(唯一通路)。main 自己的 `process.env` 一个字不动 | +| 归因 | `process-fence-profile.ts` `trimUntilCompiles` | 编译失败若不是 `exceeds maximum` 那道字节墙 ⇒ 一个工作区都不丢就抛,消息点名「丢工作区救不了」(§Q5 末节) | +| 判据 | `network-egress-fence.test.ts`(Electron 的 node + 真 .node + 真 seatbelt + 真代理)· `process-fence-wiring.test.ts` · `process-fence-profile.test.ts` · `sidecar-env.test.ts` · `network-egress-disclosure.test.ts` | 逃逸语料(绕代理直连 / raw-IP:443 / UDP / `[::1]` 其它端口 / DNS)逐条 EPERM 而唯一那扇门通、外面连得进被围栏的监听者;代理关掉后 fetch / CONNECT **立刻** `ECONNREFUSED` 并点名代理地址(不是挂到超时),直连仍 EPERM;控制臂(不套围栏)对同一判据必红 | +| 出货形态 | [`../verification/2026-09-10-req137-1337-packaged-egress/README.md`](../verification/2026-09-10-req137-1337-packaged-egress/README.md) | 六格 + 误伤语料 + 逃逸语料在签名包上的读数(两处 provider 目录实读包数) | + +**没有改的**:代理本体与注册表(`#1336`)、可写集的文件规则、并集裁剪规则、main / renderer 的出网。非 darwin 没有围栏, +也不装策略层(那里的引擎本来就没有围栏,不假装有一半)。 + +### 覆盖面声明(AC5) + + +**这道网络围栏只罩引擎 sidecar 那棵进程树**(引擎自己的模型请求、模型目录、远程 MCP、两处 provider 的 npm 安装, +以及它派生的 shell 工具 / `!command` / MCP stdio / LSP / PTY 终端)。**Electron main**(平台模型目录拉取、登录 / token、 +自动更新)**与 renderer 的出网不在覆盖内**,装上策略层之后其行为一点没变(§Q1.4 实测)。不得宣称比这更大的保护面。 + + +用户眼前的同一句话在终端「沙箱开启」悬停卡的第四句(`packages/ui-mac/src/renderer/i18n/{zh,en}.ts` +`alpha.terminal.sandboxHoverBody`)。上面这段与那两句文案都由 `packages/ui-mac/src/main/network-egress-disclosure.test.ts` +守着存在与内容:删掉、或改成「全部出网都被限制」一类的过度声明,判据当场红。 + ## 用法 本文与老勘破一起,是 `#1073`(REQ-137)AC 与实现票的对照物。三条直接约束: diff --git a/docs/verification/2026-09-10-req137-1337-packaged-egress/README.md b/docs/verification/2026-09-10-req137-1337-packaged-egress/README.md new file mode 100644 index 000000000000..7af6beb1a179 --- /dev/null +++ b/docs/verification/2026-09-10-req137-1337-packaged-egress/README.md @@ -0,0 +1,116 @@ +--- +title: REQ-137 把路封死 —— 出货形态下围栏只放行策略代理那一扇门的取证(alpha-code#1337) +kind: verification +status: active +owners: + - alpha-code desktop maintainers +last_reviewed: 2026-09-10 +--- + +# alpha-code#1337 · 出货形态:六格工作负载 + 误伤语料 + 逃逸语料 + +票:[alpha-code#1337](https://github.com/jinjunnn/alpha-code/issues/1337) · +父需求:[alpha-code#1073](https://github.com/jinjunnn/alpha-code/issues/1073)(REQ-137)· +落地形状与覆盖面声明:[`docs/architecture/2026-09-10-network-egress-on-process-fence.md`](../../architecture/2026-09-10-network-egress-on-process-fence.md) §落地 · +被闭合的勘破:同文 §Q1–Q5(`#1334`,取证在 [`../2026-09-10-req159-1334-packaged-network-fence/README.md`](../2026-09-10-req159-1334-packaged-network-fence/README.md))· +隔离与驱动的先例:[`../2026-09-10-req159-1323-packaged-fence/README.md`](../2026-09-10-req159-1323-packaged-fence/README.md) + +本目录只有取证脚本 [`run.ts`](run.ts) 与结果 JSON [`results/shipped.json`](results/shipped.json)。 +**没有补丁、没有实验开关、不设任何代理 env、不起外部代理** —— 被测的就是出货的那份字节: +网络行由生产渲染器渲染,策略代理由 Electron main 自己起,sidecar 的代理变量由 main 改写。 + +## 0. 判据 + +- 网络轴只记「**流量到底通没通**」:main.log 里产品自己的代理记录(`network egress {…}`,每条 CONNECT 的 + allow / deny 与原因)、HTTP 状态码、provider 目录里到底有没有包。**不记有没有报错。** +- 文件轴仍记「探针文件到底落没落盘」(runner 进程 `existsSync` 实读)。 +- 每条会派生进程的探针第一句 `echo AC1337-STARTED`;看不见它就不许把「没读数」读成「被拦住了」。 +- 逃逸语料每条自带进程已启动的证据与 curl / node **自己报的** errno / 错误文本;`$?` 经引擎 shell 工具读回恒 0 + (两轮实测,未追根),所以退出码只记不判。 +- 内核 Sandbox 拒绝日志(`log show … deny(`)跑完后按时间窗抓一次,只做旁证。 + +## 1. 结论(`results/shipped.json`) + +| | 读数 | +| --- | --- | +| runner 汇总 | **47 pass / 0 fail / 3 obs** | +| 被测件 | `com.tide.alphacode` 0.1.12,Developer ID + hardened runtime(app 与 `alpha_fence.node` 都读到 `flags=0x10000(runtime)` / `TeamIdentifier=RQX6X6A635`),`lipo -archs` = `x86_64 arm64`,`codesign --verify --deep --strict` exit 0;asar 里 `network egress policy proxy listening` ×1(生产接线的字节在场)、`ALPHA_AC1334_NETWORK` ×0(不是 `#1334` 那份实验字节) | +| 格 1 冷启动 | health **200**;main.log:`network egress policy proxy listening on 127.0.0.1:

` → `process fence planned: … egressProxyPort=

, profile=2531B` → server.log `process fence applied … profile=2531B`(同一个端口进了 profile;`#1334` `sec5` 臂那种「装上了但引擎 560 ms 死」没有出现) | +| 格 1 sidecar env | shell 工具里实读:`HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` = `http://127.0.0.1:

`,`NO_PROXY` / `no_proxy` = `127.0.0.1,localhost,::1`;runner 自己**没设**任何代理 env —— 这份值只能来自 main 的改写 | +| 格 2 连接器(MCP stdio) | connected;界内落盘、界外 0 | +| 格 2 **provider 安装**(`#1334` 那条静默失败格) | **26 包 / 62 636 KB × 2 处**(`/opencode/node_modules` 与 `/alpha-engine-config/node_modules`,`@opencode-ai/plugin/package.json` 实读在场),`~/.npm/_cacache` 在;`registry.npmjs.org:443` 经代理 allow ≥15 条、deny 0 条;引擎日志里零 `NpmInstallFailedError` | +| 格 3 开终端(登录 shell) | 通;终端里 `$HTTPS_PROXY` 也是 `http://127.0.0.1:

`;界外写 `zsh: operation not permitted` | +| 格 4 shell 工具 | 通;界内落盘、界外 0、onboarding 根的兄弟目录 0 | +| 格 5 写配置 + ext 装载 | `alpha.jsonc` 在、`alpha-engine-config/` 在、`alpha_register` 在 | +| 格 6 三工作区并集(SIGKILL → self-heal → 重算) | `workspaces=3`,`profile=2715B`,第二代 `egressProxyPort=

` **与第一代相同**,`proxy listening` 行全程只有 **1** 条(代理跨代复用);并集重算后**第一条 shell 命令 100 ms 返回**(上一轮 98 ms;`#1334` `deny` 臂是走满 120 s 超时) | +| AC3 误伤语料(12 条) | 文件轴九条全通;`curl https://registry.npmjs.org/semver/latest` **`CURL=200`**;`npm view semver version` **`7.8.5`**;`git ls-remote https://github.com/git/git HEAD` **`b8242b0…`**;`node dns.lookup("example.com")` **`ENOTFOUND`**(预期:解析在代理那一侧,围栏里 DNS 不通,`#1334` Q3) | +| AC1 逃逸语料(6 条 + 引擎进程内 2 条) | 见 §2 | +| 真 HOME 六个哨兵 mtime 前后 | 逐字相同 | +| 残留进程 | 0 | + +## 2. 逃逸语料(AC1 反臂):每条都「进程起了、然后响亮地失败」 + +| 语料 | 发起 | 读数 | 代理侧记录 | +| --- | --- | --- | --- | +| `curl https://example.com`(未登记目的地,经代理) | shell 工具 | `curl: (56) CONNECT tunnel failed, response 403` | `deny/unregistered example.com:443` ×1 | +| `curl --noproxy '*' https://1.1.1.1/`(绕代理,raw-IP:443) | shell 工具 | `curl: (7) Failed to connect to 1.1.1.1 port 443 after 0 ms` | —(没到代理;内核 `deny(1) network-outbound remote:*:443`) | +| `curl --noproxy '*' https://github.com/`(绕代理,按名字) | shell 工具 | `curl: (6) Could not resolve host: github.com` | —(死在 DNS:内核 `deny(1) network-outbound /private/var/run/mDNSResponder`) | +| `node net.connect(443,"1.1.1.1")` | shell 工具 → node | `TCP=EPERM` | — | +| `node dgram.send(… 53,"1.1.1.1")` | shell 工具 → node | `UDP=EPERM` | — | +| `node net.connect({host:"::1",port:<空闲端口>})` | shell 工具 → node | `V6=EPERM`(loopback 上非代理端口) | — | +| 远程 MCP `http://ac1337-probe.invalid/mcp` | **引擎进程内 fetch** | `fetch failed: … Proxy response (403) !== 200 when HTTP Tunneling` | `deny/unregistered ac1337-probe.invalid:80` ×2 | +| 远程 MCP `https://ac1337-probe.invalid/mcp` | **引擎进程内 fetch** | 同上 | `deny/unregistered ac1337-probe.invalid:443` ×2 | + +最后两行是 `#1334` Q4 那条探针的升级:当时(取证替身代理)读到的是代理侧 `ENOTFOUND`,现在是**策略拒绝 403** +—— 引擎自己的出网确实经过闸门,而且闸门说的是「不在注册表」,不是「解析不了」。 + +## 3. 代理记录(main.log,按目的地) + +| 目的地 | 裁决 | 条数 | +| --- | --- | --- | +| `registry.npmjs.org:443` | allow | 15–18(两处 provider 安装 + `npm view` + `curl`) | +| `github.com:443` | allow | 2–3(`git ls-remote`) | +| `release-assets.githubusercontent.com:443` | **allow** | 1 | +| `ac1337-probe.invalid:80` / `:443` | deny/unregistered | 2 / 2 | +| `example.com:443` | deny/unregistered | 1 | + +`release-assets.githubusercontent.com:443` 那一行是本轮加进注册表的(`#1073` owner 裁决二):第一轮复跑时它还不在表里, +同一工作负载下被 **403 ×2**(shell 工具子进程发起,与 `#1334` Q4 实拍一致)—— 封路之后这类下载会被拒,属于误伤;加进表后同一负载 allow ×1。 +`example.com` 按裁决一**不加**:它只是探针靶子,AC3 语料改用 `registry.npmjs.org`。 + +## 4. 测量口径 + +| | | +| --- | --- | +| 树 | `.worktrees/ac-1337`,base = `origin/alpha` `59b26f136`(`#1336` 已合) | +| 被测产物 | `packages/ui-mac/dist/mac-arm64/Code Puppy.app`。构建 `OPENCODE_CHANNEL=prod bun run --cwd packages/ui-mac build`(exit 0,3 行 `✓ built in`);打包同 U1 §7 的发版命令,只用 CLI 覆盖关公证 / 换 `dir` target(`ALPHA_SIGN=1`) | +| addon buildId | `.node` 里烤的与 sidecar 日志 `process fence applied: addon=fence-…` 逐字相同(见 JSON `identity.addonBuildId`) | +| 隔离 | 逐条照 `#1323` README §2.1–§2.3:一次性 `~/.ac1337-shipped-XXXXXX/`,`OPENCODE_TEST_ONBOARDING=1` + 启动时 `TMPDIR=/tmp` + 隔离 `.zshrc` 只把生产 `TMPDIR` export 回去;实读 sidecar 的 `$HOME` / `$TMPDIR` / `$XDG_CONFIG_HOME` 才开始判 | +| 宿主 | macOS Darwin 25.3.0 arm64;`/bin/zsh`;runner 跑在 bun 1.3.14;`navigator.userAgent` 读回 `Electron/42.3.3` | +| 网络拓扑 | 系统代理关;DNS 答 fake-IP —— 与 `#1334` 同 | + +## 5. 跑法 + +```bash +OPENCODE_CHANNEL=prod bun run --cwd packages/ui-mac build # 核对 3 行 `✓ built in` +cd packages/ui-mac && OPENCODE_CHANNEL=prod ALPHA_SIGN=1 ./node_modules/.bin/electron-builder --mac \ + -c.mac.notarize=false -c.mac.target=dir --config electron-builder.config.ts +cd - && bun docs/verification/2026-09-10-req137-1337-packaged-egress/run.ts \ + --app "packages/ui-mac/dist/mac-arm64/Code Puppy.app" # 退出码 = 有没有 FAIL;不要用 `cmd | tail; echo $?` 读它 +``` + +`dist/` 不入仓。一轮约 7 分钟(冷启动 + 后台 provider 安装 + 一次 self-heal respawn + 20 条 shell 语料)。 + +## 6. 没测的(如实记账) + +1. **「杀掉代理」在出货形态上到不了**:代理住在 main 进程内,main 死则 sidecar 一起死(utilityProcess)。 + AC4「代理不在场时快速响亮地失败」的端到端判据在 + `packages/ui-mac/src/main/network-egress-fence.test.ts` A3(Electron 的 node + 真 seatbelt + 真代理,关掉代理再测: + fetch / CONNECT 立刻 `ECONNREFUSED 127.0.0.1:

`,直连仍 EPERM)。 +2. **没有发过一次真的模型请求**(与 `#1321` / `#1323` / `#1334` 同一条)。隔离环境没有凭据,`/config/providers` 读回 0 个 provider。 +3. **模型目录 `models.opencode.ai` 那条 fetch 本轮仍没有发生**(`/opencode/models.json` 不存在;代理记录里没有它)。 +4. `renderer.home.catalog_ready` / `catalog_liveness.confirmed` 这两条 `#1334` §4 的时序读数本轮**没取到**(main.log 里没有那两个键; + runner 只 grep 了 main.log)。能替代的一条:并集重算后第一条 shell 命令 100 ms / 98 ms(两轮;`#1334` `deny` 臂 120 s 超时)—— 按《时序门需 ≥3 轮采样》仍不构成结论。 +5. **单机单配置单拓扑**;公证 / staple 没做;x86_64 那一片没有被执行过。 +6. `127.0.0.1:11434`(ollama)在注册表里,但围栏只放行 `localhost:

` 且 `NO_PROXY` 含 `127.0.0.1` ⇒ 客户端会直连 ⇒ EPERM。 + 本轮没有 ollama 负载,**未测**;它是注册表与 profile 之间的一条已知张力,归 `#1073` 裁(加一行 `(allow network-outbound (remote ip "localhost:11434"))` 或删表行)。 diff --git a/docs/verification/2026-09-10-req137-1337-packaged-egress/results/shipped.json b/docs/verification/2026-09-10-req137-1337-packaged-egress/results/shipped.json new file mode 100644 index 000000000000..fd5661555ca9 --- /dev/null +++ b/docs/verification/2026-09-10-req137-1337-packaged-egress/results/shipped.json @@ -0,0 +1,1534 @@ +{ + "ticket": "alpha-code#1337", + "arm": "shipped", + "identity": { + "ticket": "alpha-code#1337", + "arm": "shipped", + "app": "packages/ui-mac/dist/mac-arm64/Code Puppy.app", + "bundleId": "com.tide.alphacode", + "version": "0.1.12", + "codesignApp": [ + "Identifier=com.tide.alphacode", + "CodeDirectory v=20500 size=446 flags=0x10000(runtime) hashes=3+7 location=embedded", + "Authority=Developer ID Application: Beijing yuanyuji Technology Co.,Ltd (RQX6X6A635)", + "Authority=Developer ID Certification Authority", + "TeamIdentifier=RQX6X6A635" + ], + "codesignAddon": [ + "Identifier=alpha_fence", + "CodeDirectory v=20500 size=311 flags=0x10000(runtime) hashes=4+2 location=embedded", + "Authority=Developer ID Application: Beijing yuanyuji Technology Co.,Ltd (RQX6X6A635)", + "Authority=Developer ID Certification Authority", + "TeamIdentifier=RQX6X6A635" + ], + "lipoAddon": "x86_64 arm64", + "verifyDeepStrictExit": 0, + "addonBuildId": [ + "fence-20260910T100303262Z" + ], + "addonSha256": "3d4801a546802098c5897fde3cf78c2582868d237129edbd0b06020a084339fe", + "asarMarkers": { + "network egress policy proxy listening": 1, + "ALPHA_AC1334_NETWORK(control: must be 0)": 0, + "AC1337-NONEXISTENT-NEEDLE(control)": 0 + }, + "orphansBefore": [], + "ownerAppPidsUntouched": [], + "iso": "", + "home": "/home", + "launchTmp": "/tmp", + "defaultWorkspace": "/home/code-puppy", + "wsA": "/ws-a", + "wsB": "/ws-b", + "wsC": "/ws-c", + "esc": "/esc", + "realTmpdir": "/var/folders/9m/lxt29cf574n04rxx3t6s3gs00000gn/T/", + "launchEnvSubset": { + "HOMEBREW_PREFIX": "/opt/homebrew", + "HOMEBREW_CELLAR": "/opt/homebrew/Cellar", + "HOMEBREW_REPOSITORY": "/opt/homebrew", + "HOME": "/home", + "TMPDIR": "/tmp", + "OPENCODE_TEST_ONBOARDING": "1", + "OPENCODE_PORT": "55085" + }, + "appPid": 49400, + "ports": { + "engine": 55085, + "cdp": 55086 + }, + "onboardingRoot": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013", + "userData": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop", + "alphaGlobalRoot": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod", + "xdgConfig": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/config", + "xdgData": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/data", + "xdgCache": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/cache", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) alpha-code/0.1.12 Chrome/148.0.7778.218 Electron/42.3.3 Safari/537.36", + "logRun": "20260910T101038", + "appStartingLine": "[2026-09-10 06:10:38.483] [info] app starting { version: '0.1.12', packaged: true, onboardingTest: true }", + "fenceMainLines": [ + "[2026-09-10 06:10:38.497] [info] [server] Loaded shell environment with -il (12 vars)", + "[2026-09-10 06:10:38.498] [info] (main) req047: rebuilt shell env cache after successful probe", + "[2026-09-10 06:10:38.606] [info] spawning sidecar { url: 'http://127.0.0.1:55085' }", + "[2026-09-10 06:10:38.609] [info] network egress policy proxy listening on 127.0.0.1:55088 — the only way out of the engine tree (REQ-137); destinations = network-egress-registry.ts", + "[2026-09-10 06:10:38.617] [info] process fence planned: workspaces=1 (candidates=1, excluded=0, dropped=0), egressProxyPort=55088, profile=2531B, compile attempts=1" + ], + "fenceServerLines": [ + " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2531B — every process this engine spawns inherits it'" + ], + "egressProxyPort": 55088, + "engineUrl": "http://127.0.0.1:55085", + "agent": "build" + }, + "escapeDirListing": [], + "launchTmpListing": [ + "opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013" + ], + "defaultWsListing": [ + "d1", + "fp-git", + "g.txt", + "inside.txt", + "mcp-shipped.txt", + "multi-shipped.txt", + "pty-default-shipped.txt", + "shelltool-shipped.txt" + ], + "wsAListing": [ + "multi-shipped.txt" + ], + "wsBListing": [ + "multi-shipped.txt" + ], + "wsCListing": [], + "gateRecords": [ + { + "event": "egress.connect", + "id": 1, + "at": "2026-09-10T10:10:39.267Z", + "method": "CONNECT", + "authority": "github.com:443", + "host": "github.com", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 2, + "at": "2026-09-10T10:10:39.628Z", + "method": "CONNECT", + "authority": "release-assets.githubusercontent.com:443", + "host": "release-assets.githubusercontent.com", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 3, + "at": "2026-09-10T10:10:39.648Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 4, + "at": "2026-09-10T10:10:39.649Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 5, + "at": "2026-09-10T10:10:43.048Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 6, + "at": "2026-09-10T10:10:43.049Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 7, + "at": "2026-09-10T10:10:43.049Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 8, + "at": "2026-09-10T10:10:43.098Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 9, + "at": "2026-09-10T10:10:43.098Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 10, + "at": "2026-09-10T10:10:43.099Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 11, + "at": "2026-09-10T10:10:44.006Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 12, + "at": "2026-09-10T10:10:44.006Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 14, + "at": "2026-09-10T10:10:44.008Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 13, + "at": "2026-09-10T10:10:44.008Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 15, + "at": "2026-09-10T10:10:44.009Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 17, + "at": "2026-09-10T10:10:44.124Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 16, + "at": "2026-09-10T10:10:44.124Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 18, + "at": "2026-09-10T10:11:04.581Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 20, + "at": "2026-09-10T10:11:05.144Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 19, + "at": "2026-09-10T10:11:05.144Z", + "method": "CONNECT", + "authority": "registry.npmjs.org:443", + "host": "registry.npmjs.org", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 21, + "at": "2026-09-10T10:11:05.670Z", + "method": "CONNECT", + "authority": "github.com:443", + "host": "github.com", + "port": 443, + "verdict": "allow", + "status": 200 + }, + { + "event": "egress.connect", + "id": 22, + "at": "2026-09-10T10:11:06.895Z", + "method": "CONNECT", + "authority": "example.com:443", + "host": "example.com", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 23, + "at": "2026-09-10T10:11:07.133Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:80", + "host": "ac1337-probe.invalid", + "port": 80, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 24, + "at": "2026-09-10T10:11:07.138Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:80", + "host": "ac1337-probe.invalid", + "port": 80, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 25, + "at": "2026-09-10T10:11:11.153Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:443", + "host": "ac1337-probe.invalid", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 26, + "at": "2026-09-10T10:11:11.157Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:443", + "host": "ac1337-probe.invalid", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + } + ], + "sandboxDenyLines": [ + "2026-09-10 06:10:38.481 E kernel[0:4839005] (Sandbox) Sandbox: logd_helper(82563) deny(1) file-read-data /app/alpha-code/.worktrees/ac-1337/packages/ui-mac/dist/mac-arm64/Code Puppy.app/Contents/Resources/app.asar.unpacked/node_modules/@lydell/node-pty-darwin-arm64/prebuilds/darwin-arm64/pty.node", + "2026-09-10 06:10:38.616 E kernel[0:493c413] (Sandbox) Sandbox: true(49408) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:38.862 E kernel[0:493bce2] (Sandbox) Sandbox: sharingd(783) deny(1) syscall-unix 545", + "2026-09-10 06:10:39.051 E kernel[0:493c220] (Sandbox) 31 duplicate reports for Sandbox: sharingd(783) deny(1) syscall-unix 545", + "2026-09-10 06:10:39.051 E kernel[0:493c220] (Sandbox) Sandbox: nsattributedstringagent(89799) deny(1) mach-lookup com.apple.CARenderServer", + "2026-09-10 06:10:40.358 E kernel[0:493c505] (Sandbox) Sandbox: bsdtar(49415) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:49.516 E kernel[0:493c5ec] (Sandbox) Sandbox: zsh(49464) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:49.521 E kernel[0:493c5ee] (Sandbox) Sandbox: path_helper(49465) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:49.578 E kernel[0:493c5df] (Sandbox) Sandbox: sharingd(783) deny(1) syscall-unix 545", + "2026-09-10 06:10:49.777 E kernel[0:493c206] (Sandbox) 31 duplicate reports for Sandbox: sharingd(783) deny(1) syscall-unix 545", + "2026-09-10 06:10:51.612 E kernel[0:493c71b] (Sandbox) Sandbox: rg(49493) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:51.623 E kernel[0:493c729] (Sandbox) Sandbox: spawn-helper(49494) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:51.626 E kernel[0:493c72b] (Sandbox) Sandbox: zsh(49494) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:51.632 E kernel[0:493c72d] (Sandbox) Sandbox: path_helper(49495) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:51.636 E kernel[0:493c72f] (Sandbox) Sandbox: locale(49496) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:54.135 E kernel[0:493c72b] (Sandbox) Sandbox: zsh(49494) deny(1) file-write-create /esc/pty-default-shipped.txt", + "2026-09-10 06:10:59.153 E kernel[0:493c7ba] (Sandbox) Sandbox: zsh(49535) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:59.158 E kernel[0:493c7be] (Sandbox) Sandbox: path_helper(49537) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:10:59.160 E kernel[0:493c7ba] (Sandbox) Sandbox: zsh(49535) deny(1) file-write-create /esc/shelltool-shipped.txt", + "2026-09-10 06:10:59.160 E kernel[0:493c7ba] (Sandbox) Sandbox: zsh(49535) deny(1) file-write-create /tmp/outside-root-shipped.txt", + "2026-09-10 06:10:59.252 E kernel[0:4839005] (Sandbox) Sandbox: logd_helper(82563) deny(1) file-read-data /app/alpha-code/.worktrees/ac-1337/packages/ui-mac/dist/mac-arm64/Code Puppy.app/Contents/Resources/app.asar.unpacked/node_modules/@lydell/node-pty-darwin-arm64/prebuilds/darwin-arm64/pty.node", + "2026-09-10 06:10:59.252 E kernel[0:4839005] (Sandbox) Sandbox: logd_helper(82563) deny(1) file-read-data /app/alpha-code/.worktrees/ac-1337/packages/ui-mac/dist/mac-arm64/Code Puppy.app/Contents/Resources/app.asar.unpacked/node_modules/@parcel/watcher-darwin-arm64/watcher.node", + "2026-09-10 06:11:00.889 E kernel[0:493c7e8] (Sandbox) Sandbox: true(49541) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:00.993 E kernel[0:493c7e9] (Sandbox) Sandbox: Code Puppy Helper(49542) deny(1) file-write-create /private/var/tmp/alpha-fence-probe-49542-1789035060993-j5l5ez", + "2026-09-10 06:11:01.534 E kernel[0:493c811] (Sandbox) Sandbox: rg(49550) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.564 E kernel[0:493c825] (Sandbox) Sandbox: git(49553) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.594 E kernel[0:493c826] (Sandbox) Sandbox: xcodebuild(49554) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.637 E kernel[0:493c85b] (Sandbox) Sandbox: git(49553) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.640 E kernel[0:493c85e] (Sandbox) Sandbox: git(49557) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.651 E kernel[0:493c862] (Sandbox) 1 duplicate report for Sandbox: git(49557) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.651 E kernel[0:493c862] (Sandbox) Sandbox: git(49560) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.663 E kernel[0:493c866] (Sandbox) 1 duplicate report for Sandbox: git(49560) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.663 E kernel[0:493c866] (Sandbox) Sandbox: git(49563) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.676 E kernel[0:493c86a] (Sandbox) 1 duplicate report for Sandbox: git(49563) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.676 E kernel[0:493c86a] (Sandbox) Sandbox: git(49566) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.691 E kernel[0:493c86e] (Sandbox) 1 duplicate report for Sandbox: git(49566) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.697 E kernel[0:493bf3c] (Sandbox) Sandbox: searchpartyd(622) deny(1) mach-lookup com.apple.ak.anisette.xpc", + "2026-09-10 06:11:01.699 E kernel[0:493c882] (Sandbox) 1 duplicate report for Sandbox: searchpartyd(622) deny(1) mach-lookup com.apple.ak.anisette.xpc", + "2026-09-10 06:11:01.699 E kernel[0:493c882] (Sandbox) Sandbox: git(49570) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.712 E kernel[0:493c886] (Sandbox) 1 duplicate report for Sandbox: git(49570) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.712 E kernel[0:493c886] (Sandbox) Sandbox: git(49573) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.723 E kernel[0:493c88a] (Sandbox) 1 duplicate report for Sandbox: git(49573) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.723 E kernel[0:493c88a] (Sandbox) Sandbox: git(49576) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.736 E kernel[0:493c88f] (Sandbox) 1 duplicate report for Sandbox: git(49576) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.736 E kernel[0:493c88f] (Sandbox) Sandbox: git(49579) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.746 E kernel[0:493c893] (Sandbox) 1 duplicate report for Sandbox: git(49579) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.746 E kernel[0:493c893] (Sandbox) Sandbox: git(49582) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.756 E kernel[0:493c897] (Sandbox) 1 duplicate report for Sandbox: git(49582) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:01.756 E kernel[0:493c897] (Sandbox) Sandbox: git(49585) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.288 E kernel[0:493c8c2] (Sandbox) 1 duplicate report for Sandbox: git(49585) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.288 E kernel[0:493c8c2] (Sandbox) Sandbox: git(49599) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.308 E kernel[0:493c8c6] (Sandbox) 1 duplicate report for Sandbox: git(49599) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.308 E kernel[0:493c8c6] (Sandbox) Sandbox: git(49602) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.321 E kernel[0:493c8ca] (Sandbox) 1 duplicate report for Sandbox: git(49602) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.321 E kernel[0:493c8ca] (Sandbox) Sandbox: git(49605) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.333 E kernel[0:493c8ce] (Sandbox) 1 duplicate report for Sandbox: git(49605) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.333 E kernel[0:493c8ce] (Sandbox) Sandbox: git(49608) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.344 E kernel[0:493c8d2] (Sandbox) 1 duplicate report for Sandbox: git(49608) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.344 E kernel[0:493c8d2] (Sandbox) Sandbox: git(49611) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.365 E kernel[0:493c8d8] (Sandbox) 1 duplicate report for Sandbox: git(49611) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.365 E kernel[0:493c8d8] (Sandbox) Sandbox: git(49614) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.365 E kernel[0:493c8db] (Sandbox) Sandbox: git(49617) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.372 E kernel[0:493c8dc] (Sandbox) Sandbox: zsh(49618) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.375 E kernel[0:493c8de] (Sandbox) Sandbox: git(49617) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.375 E kernel[0:493c8dd] (Sandbox) Sandbox: git(49614) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.376 E kernel[0:493c8e1] (Sandbox) Sandbox: path_helper(49619) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.377 E kernel[0:493c8dc] (Sandbox) Sandbox: zsh(49618) deny(1) file-write-create /ws-c/multi-shipped.txt", + "2026-09-10 06:11:04.380 E kernel[0:493c8e5] (Sandbox) Sandbox: git(49622) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.387 E kernel[0:493c8e6] (Sandbox) Sandbox: zsh(49623) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.388 E kernel[0:493c8e7] (Sandbox) Sandbox: git(49622) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.391 E kernel[0:493c8e9] (Sandbox) Sandbox: path_helper(49624) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.391 E kernel[0:493c8ec] (Sandbox) Sandbox: git(49627) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.396 E kernel[0:493c8ee] (Sandbox) Sandbox: ls(49628) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.399 E kernel[0:493c8ef] (Sandbox) Sandbox: git(49627) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.405 E kernel[0:493c8f7] (Sandbox) Sandbox: zsh(49629) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.409 E kernel[0:493c8fa] (Sandbox) Sandbox: path_helper(49630) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.422 E kernel[0:493c8fb] (Sandbox) Sandbox: zsh(49631) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.424 E kernel[0:493c8fe] (Sandbox) Sandbox: git(49634) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.425 E kernel[0:493c900] (Sandbox) Sandbox: path_helper(49635) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.428 E kernel[0:493c902] (Sandbox) Sandbox: mkdir(49636) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.430 E kernel[0:493c904] (Sandbox) Sandbox: git(49637) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.432 E kernel[0:493c905] (Sandbox) Sandbox: git(49634) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.435 E kernel[0:493c908] (Sandbox) Sandbox: git(49640) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.437 E kernel[0:493c909] (Sandbox) Sandbox: git(49637) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.442 E kernel[0:493c90a] (Sandbox) Sandbox: git(49640) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.444 E kernel[0:493c90e] (Sandbox) Sandbox: git(49644) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.444 E kernel[0:493c90f] (Sandbox) Sandbox: git(49641) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.452 E kernel[0:493c910] (Sandbox) Sandbox: git(49644) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.452 E kernel[0:493c911] (Sandbox) Sandbox: git(49641) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.454 E kernel[0:493c914] (Sandbox) Sandbox: git(49647) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.456 E kernel[0:493c916] (Sandbox) Sandbox: git(49648) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.462 E kernel[0:493c917] (Sandbox) Sandbox: git(49647) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.462 E kernel[0:493c918] (Sandbox) Sandbox: git(49648) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.464 E kernel[0:493c91b] (Sandbox) Sandbox: git(49651) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.468 E kernel[0:493c91d] (Sandbox) Sandbox: git(49652) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.471 E kernel[0:493c91e] (Sandbox) Sandbox: git(49651) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.472 E kernel[0:493c921] (Sandbox) Sandbox: git(49654) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.475 E kernel[0:493c922] (Sandbox) Sandbox: rg(49655) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.480 E kernel[0:493c930] (Sandbox) Sandbox: git(49654) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.482 E kernel[0:493c933] (Sandbox) Sandbox: git(49658) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.489 E kernel[0:493c935] (Sandbox) Sandbox: zsh(49659) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.489 E kernel[0:493c934] (Sandbox) Sandbox: git(49658) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.492 E kernel[0:493c938] (Sandbox) Sandbox: path_helper(49660) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.492 E kernel[0:493c93b] (Sandbox) Sandbox: git(49663) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.503 E kernel[0:493c941] (Sandbox) 1 duplicate report for Sandbox: git(49663) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.503 E kernel[0:493c941] (Sandbox) Sandbox: git(49667) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.504 E kernel[0:493c93d] (Sandbox) Sandbox: node(49664) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.510 E kernel[0:493c942] (Sandbox) Sandbox: git(49667) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.514 E kernel[0:493c94b] (Sandbox) Sandbox: git(49670) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.525 E kernel[0:493c950] (Sandbox) 1 duplicate report for Sandbox: git(49670) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.525 E kernel[0:493c950] (Sandbox) Sandbox: git(49673) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.535 E kernel[0:493c954] (Sandbox) 1 duplicate report for Sandbox: git(49673) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.535 E kernel[0:493c954] (Sandbox) Sandbox: git(49676) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.548 E kernel[0:493c956] (Sandbox) 1 duplicate report for Sandbox: git(49676) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.548 E kernel[0:493c956] (Sandbox) Sandbox: zsh(49677) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.552 E kernel[0:493c958] (Sandbox) Sandbox: path_helper(49678) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.554 E kernel[0:493c95a] (Sandbox) Sandbox: head(49679) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.562 E kernel[0:493c95b] (Sandbox) Sandbox: zsh(49680) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.566 E kernel[0:493c95d] (Sandbox) Sandbox: path_helper(49681) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.568 E kernel[0:493c95f] (Sandbox) Sandbox: curl(49682) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.945 E kernel[0:493c966] (Sandbox) Sandbox: zsh(49683) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.950 E kernel[0:493c968] (Sandbox) Sandbox: path_helper(49684) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.953 E kernel[0:493c96a] (Sandbox) Sandbox: mkdir(49685) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.964 E kernel[0:493c96b] (Sandbox) Sandbox: zsh(49686) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.968 E kernel[0:493c96d] (Sandbox) Sandbox: path_helper(49687) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.971 E kernel[0:493c96f] (Sandbox) Sandbox: grep(49688) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.979 E kernel[0:493c970] (Sandbox) Sandbox: zsh(49689) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.983 E kernel[0:493c972] (Sandbox) Sandbox: path_helper(49690) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.991 E kernel[0:493c973] (Sandbox) Sandbox: zsh(49691) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.995 E kernel[0:493c975] (Sandbox) Sandbox: path_helper(49692) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.998 E kernel[0:493c977] (Sandbox) Sandbox: env(49693) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:04.998 E kernel[0:493c979] (Sandbox) Sandbox: tail(49694) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.009 E kernel[0:493c985] (Sandbox) Sandbox: node(49693) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.634 E kernel[0:493c9f0] (Sandbox) Sandbox: zsh(49695) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.638 E kernel[0:493c9f2] (Sandbox) Sandbox: path_helper(49696) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.642 E kernel[0:493c9f4] (Sandbox) Sandbox: git(49697) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.642 E kernel[0:493c9f6] (Sandbox) Sandbox: tail(49698) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.652 E kernel[0:493c9fa] (Sandbox) Sandbox: git(49697) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.656 E kernel[0:493ca01] (Sandbox) Sandbox: git(49699) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:05.659 E kernel[0:493ca03] (Sandbox) Sandbox: git-remote-http(49700) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.813 E kernel[0:493ca23] (Sandbox) Sandbox: zsh(49711) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.817 E kernel[0:493ca25] (Sandbox) Sandbox: path_helper(49712) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.831 E kernel[0:493ca27] (Sandbox) Sandbox: node(49713) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.866 E kernel[0:493ca32] (Sandbox) Sandbox: node(49713) deny(1) network-outbound /private/var/run/mDNSResponder", + "2026-09-10 06:11:06.880 E kernel[0:493ca36] (Sandbox) Sandbox: zsh(49716) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.883 E kernel[0:493ca38] (Sandbox) Sandbox: path_helper(49717) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.885 E kernel[0:493ca3a] (Sandbox) Sandbox: curl(49718) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.904 E kernel[0:493ca3e] (Sandbox) Sandbox: zsh(49719) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.908 E kernel[0:493ca40] (Sandbox) Sandbox: path_helper(49720) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.910 E kernel[0:493ca42] (Sandbox) Sandbox: curl(49721) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.916 E kernel[0:493ca42] (Sandbox) Sandbox: curl(49721) deny(1) network-outbound remote:*:443", + "2026-09-10 06:11:06.923 E kernel[0:493ca46] (Sandbox) Sandbox: zsh(49722) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.926 E kernel[0:493ca48] (Sandbox) Sandbox: path_helper(49723) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.929 E kernel[0:493ca4a] (Sandbox) Sandbox: curl(49724) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.934 E kernel[0:493ca4d] (Sandbox) Sandbox: curl(49724) deny(1) network-outbound /private/var/run/mDNSResponder", + "2026-09-10 06:11:06.942 E kernel[0:493ca4e] (Sandbox) Sandbox: zsh(49725) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.945 E kernel[0:493ca51] (Sandbox) Sandbox: path_helper(49726) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.957 E kernel[0:493ca53] (Sandbox) Sandbox: node(49727) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:06.991 E kernel[0:493ca53] (Sandbox) Sandbox: node(49727) deny(1) network-outbound remote:*:443", + "2026-09-10 06:11:07.000 E kernel[0:493ca7c] (Sandbox) Sandbox: zsh(49745) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.004 E kernel[0:493ca8d] (Sandbox) Sandbox: path_helper(49757) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.016 E kernel[0:493ca93] (Sandbox) Sandbox: node(49758) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.055 E kernel[0:493ca93] (Sandbox) Sandbox: node(49758) deny(1) network-outbound remote:*:53", + "2026-09-10 06:11:07.066 E kernel[0:493ca9f] (Sandbox) Sandbox: zsh(49759) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.070 E kernel[0:493caa1] (Sandbox) Sandbox: path_helper(49760) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.081 E kernel[0:493caa3] (Sandbox) Sandbox: node(49761) deny(1) file-write-data /dev/dtracehelper", + "2026-09-10 06:11:07.118 E kernel[0:493caa3] (Sandbox) Sandbox: node(49761) deny(1) network-outbound remote:*:55184", + "2026-09-10 06:11:15.312 E kernel[0:4839005] (Sandbox) Sandbox: logd_helper(82563) deny(1) file-read-data /app/alpha-code/.worktrees/ac-1337/packages/ui-mac/dist/mac-arm64/Code Puppy.app/Contents/Resources/app.asar.unpacked/node_modules/@lydell/node-pty-darwin-arm64/prebuilds/darwin-arm64/pty.node", + "2026-09-10 06:11:15.312 E kernel[0:4839005] (Sandbox) Sandbox: logd_helper(82563) deny(1) file-read-data /app/alpha-code/.worktrees/ac-1337/packages/ui-mac/dist/mac-arm64/Code Puppy.app/Contents/Resources/app.asar.unpacked/node_modules/@parcel/watcher-darwin-arm64/watcher.node", + "2026-09-10 06:11:15.902 E kernel[0:493bf3c] (Sandbox) Sandbox: nsattributedstringagent(89799) deny(1) mach-lookup com.apple.CARenderServer" + ], + "probes": [ + { + "id": "identity.shippingForm", + "grid": "g0", + "kind": "identity", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "codesignApp": [ + "Identifier=com.tide.alphacode", + "CodeDirectory v=20500 size=446 flags=0x10000(runtime) hashes=3+7 location=embedded", + "Authority=Developer ID Application: Beijing yuanyuji Technology Co.,Ltd (RQX6X6A635)", + "Authority=Developer ID Certification Authority", + "TeamIdentifier=RQX6X6A635" + ], + "codesignAddon": [ + "Identifier=alpha_fence", + "CodeDirectory v=20500 size=311 flags=0x10000(runtime) hashes=4+2 location=embedded", + "Authority=Developer ID Application: Beijing yuanyuji Technology Co.,Ltd (RQX6X6A635)", + "Authority=Developer ID Certification Authority", + "TeamIdentifier=RQX6X6A635" + ], + "lipo": "x86_64 arm64", + "verifyExit": 0, + "markers": { + "network egress policy proxy listening": 1, + "ALPHA_AC1334_NETWORK(control: must be 0)": 0, + "AC1337-NONEXISTENT-NEEDLE(control)": 0 + } + } + }, + { + "id": "g1.proxyListening+fencePlannedWithSamePort+applied", + "grid": "g1", + "kind": "net", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "proxyLine": "[2026-09-10 06:10:38.609] [info] network egress policy proxy listening on 127.0.0.1:55088 — the only way out of the engine tree (REQ-137); destinations = network-egress-registry.ts", + "planned": "[2026-09-10 06:10:38.617] [info] process fence planned: workspaces=1 (candidates=1, excluded=0, dropped=0), egressProxyPort=55088, profile=2531B, compile attempts=1", + "applied": " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2531B — every process this engine spawns inherits it'", + "proxyPort": 55088, + "plannedPort": 55088, + "profileBytesPlanned": "2531", + "profileBytesApplied": "2531" + } + }, + { + "id": "layout.rootsOutsideW11W12+sidecarEnv", + "grid": "g0", + "kind": "identity", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "root": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013", + "sidecarSees": { + "HOME": "/home", + "TMPDIR": "/var/folders/9m/lxt29cf574n04rxx3t6s3gs00000gn/T/", + "XDG_CONFIG_HOME": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/config" + }, + "appStartingLine": "[2026-09-10 06:10:38.483] [info] app starting { version: '0.1.12', packaged: true, onboardingTest: true }" + } + }, + { + "id": "g1.sidecarEnv.proxyStackRewrittenByMain", + "grid": "g1", + "kind": "net", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "HTTPS_PROXY": "http://127.0.0.1:55088", + "HTTP_PROXY": "http://127.0.0.1:55088", + "ALL_PROXY": "http://127.0.0.1:55088", + "NO_PROXY": "127.0.0.1,localhost,::1", + "no_proxy": "127.0.0.1,localhost,::1", + "runnerHadNoProxyEnv": true + } + }, + { + "id": "g1.coldStart.health200", + "grid": "g1", + "kind": "obs", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "health": 200 + } + }, + { + "id": "g1.coldStart.configReadable", + "grid": "g1", + "kind": "obs", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "shell": null + } + }, + { + "id": "gN.modelsDev.providerListSize", + "grid": "gN", + "kind": "net", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": null, + "detail": { + "status": 200, + "providerCount": 0, + "providerIds": [], + "modelsCacheExists": false, + "egressSoFar": [ + [ + "allow registry.npmjs.org:443", + 15 + ], + [ + "allow github.com:443", + 1 + ], + [ + "allow release-assets.githubusercontent.com:443", + 1 + ] + ] + } + }, + { + "id": "g2.mcp.connected", + "grid": "g2", + "kind": "obs", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "body": { + "ac1337probe": { + "status": "connected" + } + } + } + }, + { + "id": "g2.mcp.inside(W1 default ws)", + "grid": "g2", + "kind": "inside", + "path": "/home/code-puppy/mcp-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": {} + }, + { + "id": "g2.mcp.escape", + "grid": "g2", + "kind": "escape", + "path": "/esc/mcp-shipped.txt", + "processStarted": true, + "landed": false, + "expectLanded": false, + "ok": true, + "detail": {} + }, + { + "id": "g2.providerInstall.bothDirsPopulated(W6/W3)+npmCache(W7)+viaGate", + "grid": "g2", + "kind": "net", + "processStarted": null, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": { + "dirs": [ + { + "dir": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/config/opencode/node_modules", + "pluginInstalled": true, + "topLevel": 26, + "sizeKB": 62636 + }, + { + "dir": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/node_modules", + "pluginInstalled": true, + "topLevel": 26, + "sizeKB": 62636 + } + ], + "npmCacache": true, + "npmFailLines": [], + "npmGateRecords": { + "allow": 15, + "deny": 0 + } + } + }, + { + "id": "g3.pty.default200(W15)", + "grid": "g3", + "kind": "obs", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "body": { + "id": "pty_08acc7a65001C1T3TeX54mJSSZ", + "title": "ac1337", + "command": "/bin/zsh", + "args": [ + "-l" + ], + "cwd": "/home/code-puppy", + "status": "running", + "pid": 49494 + } + } + }, + { + "id": "g3.pty.defaultLoginShellRanACommand", + "grid": "g3", + "kind": "obs", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "output": "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\r\u001b[0m\u001b[27m\u001b[24m\u001b[Jtide@tidedeMac-mini code-puppy % \u001b[K\u001b[?2004he\becho AC1337-STARTED; echo x > \"/.ac1 \r\u001b[K3\r337-shipped-ufBJZg/home/code-puppy/pty-default-shipped.txt\"; echo x > \"/Users/ti \r\u001b[Kd\rde/.ac1337-shipped-ufBJZg/esc/pty-default-shipped.txt\"; echo \"PTY_HTTPS_PROXY=$H \r\u001b[KT\rTTPS_PROXY\"; echo AC1337-PTY-DONE\u001b[?2004l\r\r\nAC1337-STARTED\r\nzsh: operation not permitted: /esc/pty-default-shipped.txt\r\nPTY_HTTPS_PROXY=http://127.0.0.1:55088\r\nAC1337-PTY-DONE\r\n\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\r\u001b[0m\u001b[27m\u001b[24m\u001b[Jtide@tidedeMac-mini code-puppy % \u001b[K\u001b[?2004he\bexit\u001b[?2004l\r\r\n", + "ptyHttpsProxy": "http://127.0.0.1:55088" + } + }, + { + "id": "g3.pty.default.inside", + "grid": "g3", + "kind": "inside", + "path": "/home/code-puppy/pty-default-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": {} + }, + { + "id": "g3.pty.default.escape", + "grid": "g3", + "kind": "escape", + "path": "/esc/pty-default-shipped.txt", + "processStarted": true, + "landed": false, + "expectLanded": false, + "ok": true, + "detail": {} + }, + { + "id": "g4.shellTool.ranToCompletion", + "grid": "g4", + "kind": "obs", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\ndone\n(eval):1: operation not permitted: /esc/shelltool-shipped.txt\n(eval):1: operation not permitted: /tmp/outside-root-shipped.txt\n" + } + }, + { + "id": "g4.shellTool.inside(W1)", + "grid": "g4", + "kind": "inside", + "path": "/home/code-puppy/shelltool-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": {} + }, + { + "id": "g4.shellTool.escape", + "grid": "g4", + "kind": "escape", + "path": "/esc/shelltool-shipped.txt", + "processStarted": true, + "landed": false, + "expectLanded": false, + "ok": true, + "detail": {} + }, + { + "id": "g4.shellTool.escape.siblingOfOnboardingRoot", + "grid": "g4", + "kind": "escape", + "path": "/tmp/outside-root-shipped.txt", + "processStarted": true, + "landed": false, + "expectLanded": false, + "ok": true, + "detail": {} + }, + { + "id": "g5.injection.alphaGlobalRoot(W2)+engineConfig(W3)", + "grid": "g5", + "kind": "inside", + "path": "/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "processStarted": null, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": { + "alphaJsoncMtimeAfterLaunch": true, + "engineConfigEntries": [ + ".gitignore", + "models.json", + "node_modules", + "opencode.json", + "opencode.jsonc", + "package-lock.json", + "package.json" + ], + "injectionLines": [] + } + }, + { + "id": "g5.extLoadedInSidecar(alpha_register present)", + "grid": "g5", + "kind": "obs", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "alphaTools": [ + "alpha_reload", + "alpha_register", + "alpha_echo", + "alpha_ping" + ] + } + }, + { + "id": "g6.union.selfHealRespawnRePlannedThree+sameProxyPort", + "grid": "g6", + "kind": "obs", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "gen1Pids": [ + 49409 + ], + "plannedAll": [ + "[2026-09-10 06:10:38.617] [info] process fence planned: workspaces=1 (candidates=1, excluded=0, dropped=0), egressProxyPort=55088, profile=2531B, compile attempts=1", + "[2026-09-10 06:11:00.890] [info] process fence planned: workspaces=3 (candidates=3, excluded=0, dropped=0), egressProxyPort=55088, profile=2715B, compile attempts=1" + ], + "appliedAll": [ + " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2531B — every process this engine spawns inherits it'", + " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2715B — every process this engine spawns inherits it'" + ], + "profileBytesGen2": "2715", + "proxyListeningLines": 1 + } + }, + { + "id": "g6.union.wsB.inside", + "grid": "g6", + "kind": "inside", + "path": "/ws-b/multi-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": { + "status": 200, + "firstAttemptMs": 100, + "output": "AC1337-STARTED\ndone\n(eval):1: operation not permitted: /ws-c/multi-shipped.txt\n" + } + }, + { + "id": "g6.union.wsA.inside", + "grid": "g6", + "kind": "inside", + "path": "/ws-a/multi-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": {} + }, + { + "id": "g6.union.defaultWorkspace.inside", + "grid": "g6", + "kind": "inside", + "path": "/home/code-puppy/multi-shipped.txt", + "processStarted": true, + "landed": true, + "expectLanded": true, + "ok": true, + "detail": {} + }, + { + "id": "g6.union.wsC.outsideStore", + "grid": "g6", + "kind": "escape", + "path": "/ws-c/multi-shipped.txt", + "processStarted": true, + "landed": false, + "expectLanded": false, + "ok": true, + "detail": { + "output": "AC1337-STARTED\ndone\n(eval):1: operation not permitted: /ws-c/multi-shipped.txt\n" + } + }, + { + "id": "ac3.benign/workspace write (WORKDIR)", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\ninside.txt\n" + } + }, + { + "id": "ac3.benign//private/tmp write", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\n" + } + }, + { + "id": "ac3.benign/git init + commit", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "sha40": true, + "output": "AC1337-STARTED\n48d22e5408a9e8d7048b516386add383e711c940\n" + } + }, + { + "id": "ac3.benign/node writes TMPDIR", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "path": "/var/folders/9m/lxt29cf574n04rxx3t6s3gs00000gn/T/ac1337-node-49664.txt", + "output": "AC1337-STARTED\nNODEWROTE=/var/folders/9m/lxt29cf574n04rxx3t6s3gs00000gn/T/ac1337-node-49664.txt\n" + } + }, + { + "id": "ac3.benign/read repo file", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\n{\n \"$schema\": \"https://json.schemastore" + } + }, + { + "id": "ac3.benign/curl https://registry.npmjs.org (registered)", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "code": "200", + "tail": "AC1337-STARTED\nCURL=200 curl_exit=0\n", + "output": "AC1337-STARTED\nCURL=200 curl_exit=0\n" + } + }, + { + "id": "ac3.benign/mkdir -p deep", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\nMKDIR-OK\n" + } + }, + { + "id": "ac3.benign/grep", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "output": "AC1337-STARTED\n1\n" + } + }, + { + "id": "ac3.benign/which git node", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "out": "AC1337-STARTED\n/usr/bin/git\n/opt/homebrew/bin/node\n", + "output": "AC1337-STARTED\n/usr/bin/git\n/opt/homebrew/bin/node\n" + } + }, + { + "id": "ac3.benign/npm view (registry)", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "tail": "AC1337-STARTED\n7.8.5\nnpm_exit=0\n", + "output": "AC1337-STARTED\n7.8.5\nnpm_exit=0\n" + } + }, + { + "id": "ac3.benign/git ls-remote (https github.com)", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "tail": "AC1337-STARTED\nb8242b093d9e941a34460d715e3ce616a34ac3fe\tHEAD\ngit_exit=0\n", + "output": "AC1337-STARTED\nb8242b093d9e941a34460d715e3ce616a34ac3fe\tHEAD\ngit_exit=0\n" + } + }, + { + "id": "ac3.benign/dns only (expected ENOTFOUND)", + "grid": "ac3", + "kind": "benign", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "dns": "ENOTFOUND", + "output": "AC1337-STARTED\nDNS=ENOTFOUND\n" + } + }, + { + "id": "ac1.escape/unregistered destination via gate (curl https://example.com)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "curlExitAsSeen": "0", + "gateDenyRecords": [ + { + "event": "egress.connect", + "id": 22, + "at": "2026-09-10T10:11:06.895Z", + "method": "CONNECT", + "authority": "example.com:443", + "host": "example.com", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + } + ], + "tail": "AC1337-STARTED\ncurl: (56) CONNECT tunnel failed, response 403\nCURL=000 curl_exit=0\n", + "output": "AC1337-STARTED\ncurl: (56) CONNECT tunnel failed, response 403\nCURL=000 curl_exit=0\n" + } + }, + { + "id": "ac1.escape/bypass proxy, raw-IP:443 (curl --noproxy '*' https://1.1.1.1)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "curlExitAsSeen": "0", + "afterMs": "0", + "tail": "AC1337-STARTED\ncurl: (7) Failed to connect to 1.1.1.1 port 443 after 0 ms: Couldn't connect to server\nCURL=000 curl_exit=0\n", + "output": "AC1337-STARTED\ncurl: (7) Failed to connect to 1.1.1.1 port 443 after 0 ms: Couldn't connect to server\nCURL=000 curl_exit=0\n" + } + }, + { + "id": "ac1.escape/bypass proxy, by name (curl --noproxy '*' https://github.com)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "curlExitAsSeen": "0", + "tail": "AC1337-STARTED\ncurl: (6) Could not resolve host: github.com\nCURL=000 curl_exit=0\n", + "output": "AC1337-STARTED\ncurl: (6) Could not resolve host: github.com\nCURL=000 curl_exit=0\n" + } + }, + { + "id": "ac1.escape/raw TCP to 1.1.1.1:443 (node net.connect)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "tcp": "EPERM", + "output": "AC1337-STARTED\nTCP=EPERM\n" + } + }, + { + "id": "ac1.escape/UDP sendto 1.1.1.1:53 (node dgram)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "udp": "EPERM", + "output": "AC1337-STARTED\nUDP=EPERM\n" + } + }, + { + "id": "ac1.escape/[::1] other port (node net.connect)", + "grid": "ac1", + "kind": "netEscape", + "processStarted": true, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "status": 200, + "v6": "EPERM", + "port": 55184, + "output": "AC1337-STARTED\nV6=EPERM\n" + } + }, + { + "id": "ac1.engineFetch.remoteMcp.http.viaGate", + "grid": "ac1", + "kind": "net", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "url": "http://ac1337-probe.invalid/mcp", + "status": 200, + "body": "{\"ac1337remotehttp\":{\"status\":\"failed\",\"error\":\"SSE error: TypeError: fetch failed: Error: Request was cancelled.: Proxy response (403) !== 200 when HTTP Tunneling\"}}", + "gateRecords": [ + { + "event": "egress.connect", + "id": 23, + "at": "2026-09-10T10:11:07.133Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:80", + "host": "ac1337-probe.invalid", + "port": 80, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 24, + "at": "2026-09-10T10:11:07.138Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:80", + "host": "ac1337-probe.invalid", + "port": 80, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + } + ] + } + }, + { + "id": "ac1.engineFetch.remoteMcp.https.viaGate", + "grid": "ac1", + "kind": "net", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "url": "https://ac1337-probe.invalid/mcp", + "status": 200, + "body": "{\"ac1337remotehttp\":{\"status\":\"failed\",\"error\":\"SSE error: TypeError: fetch failed: Error: Request was cancelled.: Proxy response (403) !== 200 when HTTP Tunneling\"},\"ac1337remotehttps\":{\"status\":\"failed\",\"error\":\"SSE error: TypeError: fetch failed: Error: Request was cancelled.: Proxy response (403) !== 200 when HTTP Tunneling\"}}", + "gateRecords": [ + { + "event": "egress.connect", + "id": 25, + "at": "2026-09-10T10:11:11.153Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:443", + "host": "ac1337-probe.invalid", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + }, + { + "event": "egress.connect", + "id": 26, + "at": "2026-09-10T10:11:11.157Z", + "method": "CONNECT", + "authority": "ac1337-probe.invalid:443", + "host": "ac1337-probe.invalid", + "port": 443, + "verdict": "deny", + "reason": "unregistered", + "status": 403 + } + ] + } + }, + { + "id": "obs.catalogTimeline", + "grid": "obs", + "kind": "obs", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": null, + "detail": { + "lines": [], + "g6FirstShellMs": 100 + } + }, + { + "id": "identity.realHomeUntouched", + "grid": "g0", + "kind": "identity", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "before": { + "/Library/Application Support/ai.opencode.desktop": 1789008056887.3994, + "/Library/Application Support/alpha-code-state/env/prod": 1789007489376.096, + "/.npm": 1782212156775.9814, + "/.config/opencode": 1784863576481.5188, + "/.local/share/opencode": 1789023731354.3723, + "/code-puppy": 1789023529280.0942 + }, + "after": { + "/Library/Application Support/ai.opencode.desktop": 1789008056887.3994, + "/Library/Application Support/alpha-code-state/env/prod": 1789007489376.096, + "/.npm": 1782212156775.9814, + "/.config/opencode": 1784863576481.5188, + "/.local/share/opencode": 1789023731354.3723, + "/code-puppy": 1789023529280.0942 + } + } + }, + { + "id": "identity.zeroResidualProcesses", + "grid": "g0", + "kind": "identity", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": true, + "detail": { + "residual": [] + } + }, + { + "id": "gN.gateLog", + "grid": "gN", + "kind": "net", + "processStarted": null, + "landed": null, + "expectLanded": null, + "ok": null, + "detail": { + "records": 26, + "byTarget": [ + [ + "allow registry.npmjs.org:443", + 18 + ], + [ + "allow github.com:443", + 2 + ], + [ + "deny/unregistered ac1337-probe.invalid:80", + 2 + ], + [ + "deny/unregistered ac1337-probe.invalid:443", + 2 + ], + [ + "allow release-assets.githubusercontent.com:443", + 1 + ], + [ + "deny/unregistered example.com:443", + 1 + ] + ] + } + } + ], + "summary": { + "total": 50, + "pass": 47, + "fail": 0, + "observational": 3 + }, + "logs": { + "main": [ + "[2026-09-10 06:10:38.483] [info] app starting { version: '0.1.12', packaged: true, onboardingTest: true }", + "[2026-09-10 06:10:38.497] [info] [server] Loaded shell environment with -il (12 vars)", + "[2026-09-10 06:10:38.498] [info] (main) req047: rebuilt shell env cache after successful probe", + "[2026-09-10 06:10:38.606] [info] spawning sidecar { url: 'http://127.0.0.1:55085' }", + "[2026-09-10 06:10:38.609] [info] network egress policy proxy listening on 127.0.0.1:55088 — the only way out of the engine tree (REQ-137); destinations = network-egress-registry.ts", + "[2026-09-10 06:10:38.617] [info] process fence planned: workspaces=1 (candidates=1, excluded=0, dropped=0), egressProxyPort=55088, profile=2531B, compile attempts=1", + "[2026-09-10 06:11:00.267] [info] respawning sidecar { reason: 'structural' }", + "[2026-09-10 06:11:00.890] [info] process fence planned: workspaces=3 (candidates=3, excluded=0, dropped=0), egressProxyPort=55088, profile=2715B, compile attempts=1", + "[2026-09-10 06:11:01.558] [info] sidecar respawned + renderer reloaded" + ], + "server": [ + " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2531B — every process this engine spawns inherits it'", + " message: 'process fence applied: addon=fence-20260910T100303262Z libsandbox=/usr/lib/libsandbox.1.dylib profile=2715B — every process this engine spawns inherits it'" + ], + "engine": [ + "timestamp=2026-09-10T10:10:39.265Z level=INFO run=f1fd422b message=\"watcher backend\" directory=/home/code-puppy platform=darwin backend=fs-events", + "timestamp=2026-09-10T10:10:39.279Z level=INFO run=f1fd422b message=\"booting location services\" directory=/home/code-puppy workspaceID=undefined", + "timestamp=2026-09-10T10:10:39.557Z level=INFO run=f1fd422b message=\"creating instance\" directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:10:39.557Z level=INFO run=f1fd422b message=fromDirectory directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:10:39.560Z level=INFO run=f1fd422b message=bootstrapping directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:10:39.561Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/config.json", + "timestamp=2026-09-10T10:10:39.562Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:10:39.563Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:10:39.563Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "timestamp=2026-09-10T10:10:39.572Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:10:39.572Z level=INFO run=f1fd422b message=\"creating instance\" directory=/home/code-puppy", + "timestamp=2026-09-10T10:10:39.572Z level=INFO run=f1fd422b message=fromDirectory directory=/home/code-puppy", + "timestamp=2026-09-10T10:10:39.612Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:10:39.615Z level=INFO run=f1fd422b message=bootstrapping directory=/home/code-puppy", + "timestamp=2026-09-10T10:10:39.615Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "timestamp=2026-09-10T10:10:39.635Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:10:39.636Z level=INFO run=f1fd422b message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:10:49.492Z level=INFO run=f1fd422b message=created id=ses_f75338debffey5A41i7JVoTyW9 slug=silent-river version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035049492 time.updated=1789035049492", + "timestamp=2026-09-10T10:10:51.612Z level=INFO run=f1fd422b message=\"watcher backend\" directory=/home/code-puppy platform=darwin backend=fs-events", + "timestamp=2026-09-10T10:10:51.620Z level=INFO run=f1fd422b message=\"booting location services\" directory=/home/code-puppy workspaceID=undefined", + "timestamp=2026-09-10T10:10:51.621Z level=INFO run=f1fd422b message=\"creating session\" id=pty_08acc7a65001C1T3TeX54mJSSZ cmd=/bin/zsh args=\"[\\\"-l\\\"]\" cwd=/home/code-puppy", + "timestamp=2026-09-10T10:10:51.628Z level=INFO run=f1fd422b message=\"client attached to session\" id=pty_08acc7a65001C1T3TeX54mJSSZ directory=/home/code-puppy", + "timestamp=2026-09-10T10:10:59.143Z level=INFO run=f1fd422b message=created id=ses_f75336838ffergbjG2zbNBHqK6 slug=jolly-canyon version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035059143 time.updated=1789035059143", + "timestamp=2026-09-10T10:11:01.539Z level=INFO run=30692147 message=\"watcher backend\" directory=/home/code-puppy platform=darwin backend=fs-events", + "timestamp=2026-09-10T10:11:01.551Z level=INFO run=30692147 message=\"booting location services\" directory=/home/code-puppy workspaceID=undefined", + "timestamp=2026-09-10T10:11:01.589Z level=INFO run=30692147 message=\"creating instance\" directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:11:01.589Z level=INFO run=30692147 message=fromDirectory directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:11:01.591Z level=INFO run=30692147 message=bootstrapping directory=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/engine-scratch-cwd", + "timestamp=2026-09-10T10:11:01.592Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/config.json", + "timestamp=2026-09-10T10:11:01.592Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:11:01.593Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:11:01.593Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "timestamp=2026-09-10T10:11:01.595Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:11:01.596Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:11:01.683Z level=INFO run=30692147 message=\"creating instance\" directory=/home/code-puppy", + "timestamp=2026-09-10T10:11:01.683Z level=INFO run=30692147 message=fromDirectory directory=/home/code-puppy", + "timestamp=2026-09-10T10:11:01.685Z level=INFO run=30692147 message=bootstrapping directory=/home/code-puppy", + "timestamp=2026-09-10T10:11:01.685Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "timestamp=2026-09-10T10:11:01.691Z level=INFO run=30692147 message=\"watcher backend\" directory=/ws-a platform=darwin backend=fs-events", + "timestamp=2026-09-10T10:11:01.697Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:11:01.699Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:11:01.765Z level=INFO run=30692147 message=\"booting location services\" directory=/ws-a workspaceID=undefined", + "timestamp=2026-09-10T10:11:04.284Z level=INFO run=30692147 message=\"creating instance\" directory=/ws-b", + "timestamp=2026-09-10T10:11:04.284Z level=INFO run=30692147 message=fromDirectory directory=/ws-b", + "timestamp=2026-09-10T10:11:04.353Z level=INFO run=30692147 message=bootstrapping directory=/ws-b", + "timestamp=2026-09-10T10:11:04.354Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/alpha-code-state/env/prod/alpha.jsonc", + "timestamp=2026-09-10T10:11:04.357Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.json", + "timestamp=2026-09-10T10:11:04.357Z level=INFO run=30692147 message=loading path=/tmp/opencode-onboarding-a5a17508-ed7a-45f0-a228-05590c251013/desktop/alpha-engine-config/opencode.jsonc", + "timestamp=2026-09-10T10:11:04.361Z level=INFO run=30692147 message=created id=ses_f753353d6ffeTPvaJaisJ1w94O slug=glowing-knight version=1.17.13 projectID=global directory=/ws-b path=\"\" workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064361 time.updated=1789035064361", + "timestamp=2026-09-10T10:11:04.382Z level=INFO run=30692147 message=created id=ses_f753353c1ffePSirdAf1sgBqJc slug=gentle-rocket version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064382 time.updated=1789035064382", + "timestamp=2026-09-10T10:11:04.400Z level=INFO run=30692147 message=created id=ses_f753353afffeYf1xS7Zcc1mJnx slug=clever-garden version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064400 time.updated=1789035064400", + "timestamp=2026-09-10T10:11:04.416Z level=INFO run=30692147 message=created id=ses_f7533539fffeBn5iY8JrxAcvH1 slug=sunny-panda version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064416 time.updated=1789035064416", + "timestamp=2026-09-10T10:11:04.476Z level=INFO run=30692147 message=\"watcher backend\" directory=/ws-b platform=darwin backend=fs-events", + "timestamp=2026-09-10T10:11:04.484Z level=INFO run=30692147 message=created id=ses_f7533535bffeQiRNQ2qiqDqrXd slug=happy-panda version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064484 time.updated=1789035064484", + "timestamp=2026-09-10T10:11:04.543Z level=INFO run=30692147 message=created id=ses_f75335320ffe13fh3R011s7JL3 slug=cosmic-canyon version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064543 time.updated=1789035064543", + "timestamp=2026-09-10T10:11:04.547Z level=INFO run=30692147 message=\"booting location services\" directory=/ws-b workspaceID=undefined", + "timestamp=2026-09-10T10:11:04.557Z level=INFO run=30692147 message=created id=ses_f75335312ffeiaFuefkJx84ooe slug=quiet-comet version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064557 time.updated=1789035064557", + "timestamp=2026-09-10T10:11:04.937Z level=INFO run=30692147 message=created id=ses_f75335197ffeddR0N0o3lTo9He slug=lucky-panda version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064937 time.updated=1789035064937", + "timestamp=2026-09-10T10:11:04.958Z level=INFO run=30692147 message=created id=ses_f75335181ffetTVx5uyigYH02O slug=sunny-mountain version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064958 time.updated=1789035064958", + "timestamp=2026-09-10T10:11:04.974Z level=INFO run=30692147 message=created id=ses_f75335171ffewj3pVAR5pF704Y slug=glowing-meadow version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064974 time.updated=1789035064974", + "timestamp=2026-09-10T10:11:04.987Z level=INFO run=30692147 message=created id=ses_f75335164ffeo37hx81879at1U slug=kind-star version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035064987 time.updated=1789035064987", + "timestamp=2026-09-10T10:11:05.627Z level=INFO run=30692147 message=created id=ses_f75334ee4ffeWySG0NI96FnNDE slug=swift-mountain version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035065627 time.updated=1789035065627", + "timestamp=2026-09-10T10:11:06.807Z level=INFO run=30692147 message=created id=ses_f75334a48ffeAnB4DCfSCHmK3L slug=quick-squid version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066807 time.updated=1789035066807", + "timestamp=2026-09-10T10:11:06.876Z level=INFO run=30692147 message=created id=ses_f75334a03ffesBD9a1wLJBfrlq slug=misty-meadow version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066876 time.updated=1789035066876", + "timestamp=2026-09-10T10:11:06.900Z level=INFO run=30692147 message=created id=ses_f753349ebffeqEFM4c0k7hYSr2 slug=hidden-rocket version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066900 time.updated=1789035066900", + "timestamp=2026-09-10T10:11:06.919Z level=INFO run=30692147 message=created id=ses_f753349d8ffez6kZErlvxoekkt slug=quiet-meadow version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066919 time.updated=1789035066919", + "timestamp=2026-09-10T10:11:06.938Z level=INFO run=30692147 message=created id=ses_f753349c5ffe0P7lpu2uT0lO3s slug=tidy-eagle version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066938 time.updated=1789035066938", + "timestamp=2026-09-10T10:11:06.996Z level=INFO run=30692147 message=created id=ses_f7533498bffe3U8oowLh45Tk1M slug=mighty-planet version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035066996 time.updated=1789035066996", + "timestamp=2026-09-10T10:11:07.060Z level=INFO run=30692147 message=created id=ses_f7533494bffeh6wMKGOFDqVgXB slug=cosmic-comet version=1.17.13 projectID=global directory=/home/code-puppy path=Users/tide/.ac1337-shipped-ufBJZg/home/code-puppy workspaceID=undefined parentID=undefined title=\"ac1337 shipped\" agent=undefined model=undefined metadata=undefined permission=undefined cost=0 tokens.input=0 tokens.output=0 tokens.reasoning=0 tokens.cache.read=0 tokens.cache.write=0 time.created=1789035067060 time.updated=1789035067060", + "timestamp=2026-09-10T10:11:07.139Z level=WARN run=30692147 message=\"server unavailable\" key=ac1337remotehttp type=remote status=failed", + "timestamp=2026-09-10T10:11:11.158Z level=WARN run=30692147 message=\"server unavailable\" key=ac1337remotehttps type=remote status=failed" + ], + "appStderrTail": "11:01.719 (startup-timeline) › {\"seq\":68,\"name\":\"renderer.home.model_list.start\",\"t\":23259.682958,\"attempt\":1,\"chain\":1,\"source\":\"renderer\",\"rendererNow\":160.39999985694885,\"occurrence\":4}\n06:11:01.721 (startup-timeline) › {\"seq\":69,\"name\":\"renderer.home.catalog_ready\",\"t\":23261.721708,\"barrierMs\":2.4000000953674316,\"probes\":1,\"pollWaits\":0,\"probeTimeouts\":0,\"wake\":\"first\",\"source\":\"renderer\",\"rendererNow\":162.79999995231628,\"occurrence\":2}\n06:11:01.727 (startup-timeline) › {\"seq\":70,\"name\":\"renderer.home.model_list.end\",\"t\":23268.010041,\"attempt\":1,\"chain\":1,\"count\":0,\"durationMs\":8.700000047683716,\"outcome\":\"ok\",\"source\":\"renderer\",\"rendererNow\":169.09999990463257,\"occurrence\":3}\n06:11:01.766 (startup-timeline) › {\"seq\":71,\"name\":\"main.sidecar.catalog_liveness.confirmed\",\"t\":23306.206958,\"generation\":2,\"directory\":\"/ws-a\",\"elapsedMs\":209,\"probes\":1}\n06:11:04.581 › network egress {\"event\":\"egress.connect\",\"id\":18,\"at\":\"2026-09-10T10:11:04.581Z\",\"method\":\"CONNECT\",\"authority\":\"registry.npmjs.org:443\",\"host\":\"registry.npmjs.org\",\"port\":443,\"verdict\":\"allow\",\"status\":200}\n06:11:05.002 › network egress {\"event\":\"egress.tunnel-closed\",\"id\":18,\"at\":\"2026-09-10T10:11:05.002Z\",\"host\":\"registry.npmjs.org\",\"port\":443,\"bytesUp\":604,\"bytesDown\":6073}\n06:11:05.144 › network egress {\"event\":\"egress.connect\",\"id\":20,\"at\":\"2026-09-10T10:11:05.144Z\",\"method\":\"CONNECT\",\"authority\":\"registry.npmjs.org:443\",\"host\":\"registry.npmjs.org\",\"port\":443,\"verdict\":\"allow\",\"status\":200}\n06:11:05.144 › network egress {\"event\":\"egress.connect\",\"id\":19,\"at\":\"2026-09-10T10:11:05.144Z\",\"method\":\"CONNECT\",\"authority\":\"registry.npmjs.org:443\",\"host\":\"registry.npmjs.org\",\"port\":443,\"verdict\":\"allow\",\"status\":200}\n06:11:05.660 › network egress {\"event\":\"egress.tunnel-closed\",\"id\":19,\"at\":\"2026-09-10T10:11:05.660Z\",\"host\":\"registry.npmjs.org\",\"port\":443,\"bytesUp\":2087,\"bytesDown\":44673}\n06:11:05.670 › network egress {\"event\":\"egress.connect\",\"id\":21,\"at\":\"2026-09-10T10:11:05.670Z\",\"method\":\"CONNECT\",\"authority\":\"github.com:443\",\"host\":\"github.com\",\"port\":443,\"verdict\":\"allow\",\"status\":200}\n06:11:05.694 › network egress {\"event\":\"egress.tunnel-closed\",\"id\":20,\"at\":\"2026-09-10T10:11:05.694Z\",\"host\":\"registry.npmjs.org\",\"port\":443,\"bytesUp\":2037,\"bytesDown\":66148}\n06:11:06.884 › network egress {\"event\":\"egress.tunnel-closed\",\"id\":21,\"at\":\"2026-09-10T10:11:06.884Z\",\"host\":\"github.com\",\"port\":443,\"bytesUp\":893,\"bytesDown\":336525}\n06:11:06.895 › network egress {\"event\":\"egress.connect\",\"id\":22,\"at\":\"2026-09-10T10:11:06.895Z\",\"method\":\"CONNECT\",\"authority\":\"example.com:443\",\"host\":\"example.com\",\"port\":443,\"verdict\":\"deny\",\"reason\":\"unregistered\",\"status\":403}\n06:11:07.133 › network egress {\"event\":\"egress.connect\",\"id\":23,\"at\":\"2026-09-10T10:11:07.133Z\",\"method\":\"CONNECT\",\"authority\":\"ac1337-probe.invalid:80\",\"host\":\"ac1337-probe.invalid\",\"port\":80,\"verdict\":\"deny\",\"reason\":\"unregistered\",\"status\":403}\n06:11:07.138 › network egress {\"event\":\"egress.connect\",\"id\":24,\"at\":\"2026-09-10T10:11:07.138Z\",\"method\":\"CONNECT\",\"authority\":\"ac1337-probe.invalid:80\",\"host\":\"ac1337-probe.invalid\",\"port\":80,\"verdict\":\"deny\",\"reason\":\"unregistered\",\"status\":403}\n06:11:11.153 › network egress {\"event\":\"egress.connect\",\"id\":25,\"at\":\"2026-09-10T10:11:11.153Z\",\"method\":\"CONNECT\",\"authority\":\"ac1337-probe.invalid:443\",\"host\":\"ac1337-probe.invalid\",\"port\":443,\"verdict\":\"deny\",\"reason\":\"unregistered\",\"status\":403}\n06:11:11.157 › network egress {\"event\":\"egress.connect\",\"id\":26,\"at\":\"2026-09-10T10:11:11.157Z\",\"method\":\"CONNECT\",\"authority\":\"ac1337-probe.invalid:443\",\"host\":\"ac1337-probe.invalid\",\"port\":443,\"verdict\":\"deny\",\"reason\":\"unregistered\",\"status\":403}\n06:11:15.290 (utility) › sidecar exited { code: 0 }\n" + }, + "finishedAt": "2026-09-10T10:11:17.759Z" +} \ No newline at end of file diff --git a/docs/verification/2026-09-10-req137-1337-packaged-egress/run.ts b/docs/verification/2026-09-10-req137-1337-packaged-egress/run.ts new file mode 100644 index 000000000000..58e9ee88615c --- /dev/null +++ b/docs/verification/2026-09-10-req137-1337-packaged-egress/run.ts @@ -0,0 +1,815 @@ +#!/usr/bin/env bun +// REQ-137 / alpha-code#1337 —— 出货形态下「围栏只放行看门人那扇门」的取证:六格工作负载 + 误伤语料 + 逃逸语料。 +// +// bun docs/verification/2026-09-10-req137-1337-packaged-egress/run.ts \ +// --app "/Code Puppy.app" [--out results/shipped.json] [--keep] +// +// 只有一条臂:**出货的那份字节**(不打补丁、不设任何代理 env、不起外部代理)。网络行与策略代理都是生产代码 —— +// 代理在 Electron main 进程内(`#1073` 裁决三),它的结构化记录写进 main.log(`network egress {…}`),本 runner +// 从那里读「流量到底经没经那扇门」。 +// +// 判据纪律(与 #1323 / #1334 同): +// · 文件轴看「探针文件到底落没落盘」(本进程 existsSync 实读); +// · 网络轴看「流量到底通没通」—— main.log 里代理的 allow / deny 记录、HTTP 状态码、provider 目录里到底有没有包, +// 不看「有没有报错」; +// · 每条会派生进程的探针第一句 echo AC1337-STARTED,看不见它就不许把「没落盘 / 没读数」读成「被拦住了」; +// · 逃逸语料每条自带进程已启动的证据(同上)与 curl / node 自己报的 errno; +// · 内核 Sandbox 拒绝日志(log show, `deny(`)在跑完后按时间窗抓一次。 +// +// 「杀掉代理」那一臂在出货形态上**到不了**:代理住在 main 进程内,main 死则 sidecar 一起死(utilityProcess)。 +// AC4 的端到端判据在 packages/ui-mac/src/main/network-egress-fence.test.ts(Electron 的 node + 真 seatbelt + 真代理,关掉代理再测)。 +// +// 本文件不改任何生产代码。 + +import { spawn, spawnSync, type ChildProcess } from "node:child_process" +import { createServer as createNetServer } from "node:net" +import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs" +import { homedir } from "node:os" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { createHash } from "node:crypto" + +const HERE = dirname(fileURLToPath(import.meta.url)) +const REPO = join(HERE, "..", "..", "..") +const MCP_PROBE = join(REPO, "packages/ui-mac/test-fixtures/process-fence/mcp-probe.mjs") + +function arg(name: string, fallback?: string) { + const i = process.argv.indexOf(name) + return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback +} +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) + +const APP = arg("--app")! +const ARM = "shipped" +const OUT = arg("--out", join(HERE, "results", `${ARM}.json`))! +const KEEP = process.argv.includes("--keep") +const INIT_TIMEOUT = Number(arg("--init-timeout", "150000")) +if (!APP || !existsSync(APP)) throw new Error(`--app missing or not found: ${APP}`) + +type Probe = { + id: string + grid: string + kind: "identity" | "inside" | "escape" | "obs" | "net" | "benign" | "netEscape" + path?: string + processStarted: boolean | null + landed: boolean | null + expectLanded: boolean | null + ok: boolean | null + detail: unknown +} +const probes: Probe[] = [] +const identity: Record = {} +function push(p: Probe) { + probes.push(p) + const verdict = p.ok === null ? "obs " : p.ok ? "ok " : "FAIL" + console.log(`[${verdict}] ${p.grid} ${p.id} landed=${p.landed} started=${p.processStarted}${p.path ? ` ${p.path}` : ""}`) +} +const sha = (p: string) => (existsSync(p) ? createHash("sha256").update(readFileSync(p)).digest("hex") : "MISSING") +const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) + +async function freePort(): Promise { + return new Promise((res, rej) => { + const s = createNetServer() + s.on("error", rej) + s.listen(0, "127.0.0.1", () => { + const a = s.address() + if (typeof a !== "object" || !a) return rej(new Error("no port")) + const p = a.port + s.close(() => res(p)) + }) + }) +} + +// ---------------------------------------------------------------- 被测件身份 +const APP_EXEC = join(APP, "Contents/MacOS/Code Puppy") +const ADDON = join(APP, "Contents/Resources/alpha-fence/alpha_fence.node") +const ASAR = join(APP, "Contents/Resources/app.asar") +const csLines = (p: string) => + run("/usr/bin/codesign", ["-dv", "--verbose=2", p]) + .stderr.split("\n") + .filter((l) => /^(Identifier|CodeDirectory|TeamIdentifier|Authority=Developer)/.test(l)) +const asarCount = (needle: string) => Number(run("/usr/bin/grep", ["-a", "-c", needle, ASAR]).stdout.trim() || 0) +const verify = run("/usr/bin/codesign", ["--verify", "--deep", "--strict", "--verbose=1", APP]) +const plist = (k: string) => run("/usr/libexec/PlistBuddy", ["-c", `Print :${k}`, join(APP, "Contents/Info.plist")]).stdout.trim() +Object.assign(identity, { + ticket: "alpha-code#1337", + arm: ARM, + app: APP, + bundleId: plist("CFBundleIdentifier"), + version: plist("CFBundleShortVersionString"), + codesignApp: csLines(APP), + codesignAddon: csLines(ADDON), + lipoAddon: run("/usr/bin/lipo", ["-archs", ADDON]).stdout.trim(), + verifyDeepStrictExit: verify.status, + addonBuildId: [...new Set(run("/usr/bin/grep", ["-a", "-o", "fence-[0-9TZ]*", ADDON]).stdout.match(/fence-[0-9TZ]+/g) ?? [])], + addonSha256: sha(ADDON), + asarMarkers: { + // 生产接线的字节在场(main 起代理那一行)、实验开关**不在**(不是 #1334 那份实验字节)、对照针 + "network egress policy proxy listening": asarCount("network egress policy proxy listening"), + "ALPHA_AC1334_NETWORK(control: must be 0)": asarCount("ALPHA_AC1334_NETWORK"), + "AC1337-NONEXISTENT-NEEDLE(control)": asarCount("AC1337-NONEXISTENT-NEEDLE"), + }, +}) +const csApp = (identity.codesignApp as string[]).join("\n") +const csAddon = (identity.codesignAddon as string[]).join("\n") +const markers = identity.asarMarkers as Record +push({ + id: "identity.shippingForm", + grid: "g0", + kind: "identity", + processStarted: null, + landed: null, + expectLanded: null, + ok: + /flags=0x10000\(runtime\)/.test(csApp) && + /TeamIdentifier=RQX6X6A635/.test(csApp) && + /flags=0x10000\(runtime\)/.test(csAddon) && + /TeamIdentifier=RQX6X6A635/.test(csAddon) && + /arm64/.test(String(identity.lipoAddon)) && + /x86_64/.test(String(identity.lipoAddon)) && + verify.status === 0 && + markers["network egress policy proxy listening"] >= 1 && + markers["ALPHA_AC1334_NETWORK(control: must be 0)"] === 0 && + markers["AC1337-NONEXISTENT-NEEDLE(control)"] === 0, + detail: { codesignApp: identity.codesignApp, codesignAddon: identity.codesignAddon, lipo: identity.lipoAddon, verifyExit: verify.status, markers }, +}) + +// ---------------------------------------------------------------- 残留进程 +const psAll = () => run("/bin/ps", ["-axo", "pid=,ppid=,command="]).stdout.split("\n").filter(Boolean) +const OUR_DIST = /\/\.worktrees\/ac-1337\/packages\/ui-mac\/dist[^ ]*\/Code Puppy\.app/ +const orphansBefore = psAll().filter((l) => OUR_DIST.test(l)) +identity.orphansBefore = orphansBefore +for (const l of orphansBefore) { + const pid = Number(l.trim().split(/\s+/)[0]) + if (pid) { + try { + process.kill(pid, "SIGKILL") + } catch {} + } +} +identity.ownerAppPidsUntouched = psAll() + .filter((l) => l.includes("/Applications/Code Puppy.app/Contents/MacOS/Code Puppy")) + .map((l) => Number(l.trim().split(/\s+/)[0])) + +// ---------------------------------------------------------------- 隔离布局(#1323 README §2.1–2.3 逐条照做) +const REAL = homedir() +const REAL_TMPDIR = process.env.TMPDIR ?? "/tmp" +const ISO = realpathSync(mkdtempSync(join(REAL, `.ac1337-${ARM}-`))) +const HOME = join(ISO, "home") +const LAUNCH_TMP = join(ISO, "tmp") +const WS_A = join(ISO, "ws-a") +const WS_B = join(ISO, "ws-b") +const WS_C = join(ISO, "ws-c") +const ESC = join(ISO, "esc") +for (const d of [HOME, LAUNCH_TMP, WS_A, WS_B, WS_C, ESC]) mkdirSync(d, { recursive: true }) +for (const d of [WS_A, WS_B, WS_C]) run("/usr/bin/git", ["init", "-q", d]) +// 隔离 .zshrc:只把生产 TMPDIR export 回去(#1323 §2.2)。**不设任何代理 env** —— 代理变量必须由 main 改写才算数。 +writeFileSync(join(HOME, ".zshrc"), `# ac#1337 fixture (see run.ts header / #1323 README §2.2)\nexport TMPDIR=${JSON.stringify(REAL_TMPDIR)}\n`) +const DEFAULT_WS = join(HOME, "code-puppy") +Object.assign(identity, { iso: ISO, home: HOME, launchTmp: LAUNCH_TMP, defaultWorkspace: DEFAULT_WS, wsA: WS_A, wsB: WS_B, wsC: WS_C, esc: ESC, realTmpdir: REAL_TMPDIR }) + +const sentinels = [ + join(REAL, "Library/Application Support/ai.opencode.desktop"), + join(REAL, "Library/Application Support/alpha-code-state/env/prod"), + join(REAL, ".npm"), + join(REAL, ".config/opencode"), + join(REAL, ".local/share/opencode"), + join(REAL, "code-puppy"), +] +const snap = () => Object.fromEntries(sentinels.map((p) => [p.replace(REAL, ""), existsSync(p) ? statSync(p).mtimeMs : null])) +const sentinelsBefore = snap() +console.log("REAL HOME sentinels BEFORE: " + JSON.stringify(sentinelsBefore)) + +// ---------------------------------------------------------------- 起 app(不起代理:代理是产品自己的) +const ENGINE_PORT = await freePort() +const CDP_PORT = await freePort() +const env: Record = {} +for (const [k, v] of Object.entries(process.env)) { + if (v === undefined) continue + if (/^(XDG_|ALPHA_|OPENCODE_|VIRTUAL_ENV|ZDOTDIR|HOME$|TMPDIR$|HTTPS?_PROXY$|https?_proxy$|ALL_PROXY$|all_proxy$|NO_PROXY$|no_proxy$)/.test(k)) continue + env[k] = v +} +env.HOME = HOME +env.TMPDIR = LAUNCH_TMP +env.OPENCODE_TEST_ONBOARDING = "1" +env.OPENCODE_PORT = String(ENGINE_PORT) +identity.launchEnvSubset = Object.fromEntries(Object.entries(env).filter(([k]) => /^(HOME|TMPDIR|HTTPS?_PROXY|NO_PROXY|OPENCODE_)/.test(k))) +const launchedAt = Date.now() +const appLog: string[] = [] +let child: ChildProcess | undefined +let cdp: { eval: (e: string) => Promise; close: () => void } | undefined +let ROOT = "" +let USER_DATA = "" +let ALPHA_GLOBAL_ROOT = "" +let XDG_CONFIG = "" +let XDG_DATA = "" +let XDG_CACHE = "" + +async function waitForCdp(port: number, timeoutMs: number) { + const deadline = Date.now() + timeoutMs + for (;;) { + try { + const r = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(2000) }) + if (r.ok) { + const pages = ((await r.json()) as any[]).filter((t) => t.type === "page" && t.webSocketDebuggerUrl) + if (pages.length) return pages + } + } catch {} + if (child?.exitCode !== null && child?.exitCode !== undefined) throw new Error(`app exited ${child.exitCode} before CDP came up`) + if (Date.now() > deadline) throw new Error(`no CDP page on :${port}`) + await sleep(500) + } +} +async function attach(wsUrl: string) { + const ws = new WebSocket(wsUrl) + await new Promise((res, rej) => { + ws.onopen = () => res() + ws.onerror = () => rej(new Error("cdp ws error")) + }) + let id = 0 + const pending = new Map void; rej: (e: any) => void }>() + ws.onmessage = (ev) => { + const msg = JSON.parse(String(ev.data)) + const p = msg.id && pending.get(msg.id) + if (!p) return + pending.delete(msg.id) + msg.error ? p.rej(new Error(JSON.stringify(msg.error))) : p.res(msg.result) + } + return { + async eval(expression: string) { + const myId = ++id + const r = await new Promise((res, rej) => { + pending.set(myId, { res, rej }) + ws.send(JSON.stringify({ id: myId, method: "Runtime.evaluate", params: { expression, awaitPromise: true, returnByValue: true } })) + setTimeout(() => pending.has(myId) && (pending.delete(myId), rej(new Error("cdp timeout"))), 200_000) + }) + if (r.exceptionDetails) throw new Error(`eval threw: ${r.exceptionDetails.exception?.description ?? "?"}`) + return r.result?.value + }, + close: () => ws.close(), + } +} + +const logsDir = () => join(USER_DATA, "logs") +const latestRun = () => (USER_DATA && existsSync(logsDir()) ? readdirSync(logsDir()).sort().at(-1) : undefined) +const readLog = (name: string) => { + const r = latestRun() + const p = r ? join(logsDir(), r, name) : "" + return p && existsSync(p) ? readFileSync(p, "utf8") : "" +} +const grepLog = (text: string, re: RegExp, cap = 120) => text.split("\n").filter((l) => re.test(l)).slice(0, cap) +const readEngineLogs = () => { + const dir = join(XDG_DATA, "opencode", "log") + return existsSync(dir) + ? readdirSync(dir) + .filter((f) => f.endsWith(".log")) + .map((f) => readFileSync(join(dir, f), "utf8")) + .join("\n") + : "" +} +// 产品自己的代理记录(server.ts ensureEgressPolicyProxy 的 log 接线):main.log 里 `network egress {json}`。 +type EgressRecord = { event: string; id: number; at: string; method?: string; authority?: string; host?: string; port?: number; verdict?: string; reason?: string; status?: number; detail?: string } +const egressRecords = (): EgressRecord[] => + grepLog(readLog("main.log"), /network egress \{/, 5000) + .map((l) => { + const i = l.indexOf("network egress {") + try { + return JSON.parse(l.slice(i + "network egress ".length)) as EgressRecord + } catch { + return undefined + } + }) + .filter((r): r is EgressRecord => !!r && r.event === "egress.connect") +const egressByTarget = (records: EgressRecord[]) => + Object.entries( + records.reduce>((a, r) => { + const k = `${r.verdict}${r.reason ? `/${r.reason}` : ""} ${r.host ?? r.authority}:${r.port ?? ""}` + a[k] = (a[k] ?? 0) + 1 + return a + }, {}), + ).sort((a, b) => b[1] - a[1]) + +let init: { url?: string; username?: string; password?: string } | undefined +let auth: string | undefined +const api = async (method: string, path: string, directory: string, body?: unknown, timeoutMs = 120_000, extraHeaders: Record = {}) => { + const url = new URL(String(init!.url) + path) + url.searchParams.set("directory", directory) + const headers: Record = { "content-type": "application/json", "x-opencode-directory": directory, ...extraHeaders } + if (auth) headers.Authorization = auth + try { + const r = await fetch(url, { method, headers, signal: AbortSignal.timeout(timeoutMs), ...(body === undefined ? {} : { body: JSON.stringify(body) }) }) + const t = await r.text() + try { + return { status: r.status, body: JSON.parse(t) as any } + } catch { + return { status: r.status, body: t.slice(0, 800) as any } + } + } catch (e) { + return { status: 0, body: `FETCH-FAILED ${String(e)}` as any } + } +} +const health = async () => { + try { + const h = await fetch(`http://127.0.0.1:${ENGINE_PORT}/global/health`, { headers: auth ? { Authorization: auth } : {}, signal: AbortSignal.timeout(3000) }) + return h.status + } catch { + return 0 + } +} +const MARK = "AC1337-STARTED" +const q = (p: string) => `"${p}"` +const landedList = (dir: string) => (existsSync(dir) ? readdirSync(dir).filter((f) => !f.startsWith(".")).sort() : null) +function probeFile(id: string, grid: string, path: string, expectLanded: boolean, started: boolean | null, detail: unknown = {}) { + const landed = existsSync(path) + push({ id, grid, kind: expectLanded ? "inside" : "escape", path, processStarted: started, landed, expectLanded, ok: (started === null || started) && landed === expectLanded, detail }) +} +let agentName = "build" +async function shellIn(directory: string, command: string, timeoutMs = 120_000) { + const s = await api("POST", "/session", directory, { title: `ac1337 ${ARM}` }) + const sid = s.body?.id + if (!sid) return { status: s.status, output: "", raw: s.body, sid } + const r = await api("POST", `/session/${sid}/shell`, directory, { agent: agentName, model: { providerID: "opencode", modelID: "big-pickle" }, command }, timeoutMs) + const parts: any[] = Array.isArray(r.body?.parts) ? r.body.parts : [] + const tool = parts.find((p) => p?.type === "tool") ?? parts[0] + const output: string = tool?.state?.output ?? tool?.state?.metadata?.output ?? "" + return { status: r.status, output, raw: r.body, sid } +} +const sidecarPids = () => + psAll() + .filter((l) => { + const [, ppid] = l.trim().split(/\s+/) + return Number(ppid) === child?.pid && /utility-sub-type=node\.mojom\.NodeService/.test(l) + }) + .map((l) => Number(l.trim().split(/\s+/)[0])) + +try { + child = spawn(APP_EXEC, [`--remote-debugging-port=${CDP_PORT}`, "--use-mock-keychain"], { env, stdio: ["ignore", "pipe", "pipe"] }) + child.stdout?.on("data", (b) => appLog.push(b.toString())) + child.stderr?.on("data", (b) => appLog.push(b.toString())) + identity.appPid = child.pid + identity.ports = { engine: ENGINE_PORT, cdp: CDP_PORT } + + for (let i = 0; i < 160 && !ROOT; i++) { + const found = readdirSync(LAUNCH_TMP).filter((f) => f.startsWith("opencode-onboarding-")) + if (found.length === 1) ROOT = join(LAUNCH_TMP, found[0]) + else await sleep(250) + } + if (!ROOT) throw new Error(`onboarding root never appeared under ${LAUNCH_TMP}`) + USER_DATA = join(ROOT, "desktop") + ALPHA_GLOBAL_ROOT = join(ROOT, "alpha-code-state", "env", "prod") + XDG_CONFIG = join(ROOT, "config") + XDG_DATA = join(ROOT, "data") + XDG_CACHE = join(ROOT, "cache") + Object.assign(identity, { onboardingRoot: ROOT, userData: USER_DATA, alphaGlobalRoot: ALPHA_GLOBAL_ROOT, xdgConfig: XDG_CONFIG, xdgData: XDG_DATA, xdgCache: XDG_CACHE }) + + const pages = await waitForCdp(CDP_PORT, 180_000) + cdp = await attach(pages[0].webSocketDebuggerUrl) + for (let i = 0; i < 120; i++) { + const t = await cdp.eval(`typeof window.api`).catch(() => "error") + if (t === "object") break + await sleep(500) + } + identity.userAgent = await cdp.eval(`navigator.userAgent`).catch(() => null) + init = await Promise.race([cdp.eval(`window.api.awaitInitialization()`).catch(() => undefined), sleep(INIT_TIMEOUT).then(() => undefined)]) + const mainLog0 = readLog("main.log") + const serverLog0 = readLog("server.log") + identity.logRun = latestRun() + identity.appStartingLine = grepLog(mainLog0, /app starting/)[0] ?? "" + identity.fenceMainLines = grepLog(mainLog0, /process fence|fence plan|injection|spawning sidecar|sidecar exited|self-heal|respawn|shell env|Loaded shell environment|network egress policy proxy/i) + identity.fenceServerLines = grepLog(serverLog0, /process fence|sandbox_init|dlopen|fence|EPERM|not permitted|listen|EADDR/i) + const plannedLine = (identity.fenceMainLines as string[]).find((l) => /process fence planned/.test(l)) ?? "" + const appliedLine = (identity.fenceServerLines as string[]).find((l) => /process fence applied/.test(l)) ?? "" + const proxyLine = (identity.fenceMainLines as string[]).find((l) => /network egress policy proxy listening on/.test(l)) ?? "" + const proxyPort = Number(proxyLine.match(/listening on 127\.0\.0\.1:(\d+)/)?.[1] ?? 0) + const plannedPort = Number(plannedLine.match(/egressProxyPort=(\d+)/)?.[1] ?? 0) + identity.egressProxyPort = proxyPort + push({ + id: "g1.proxyListening+fencePlannedWithSamePort+applied", + grid: "g1", + kind: "net", + processStarted: null, + landed: null, + expectLanded: null, + ok: proxyPort > 0 && plannedPort === proxyPort && /process fence applied/.test(appliedLine), + detail: { proxyLine, planned: plannedLine, applied: appliedLine, proxyPort, plannedPort, profileBytesPlanned: plannedLine.match(/profile=(\d+)B/)?.[1] ?? null, profileBytesApplied: appliedLine.match(/profile=(\d+)B/)?.[1] ?? null }, + }) + + if (!init?.url) { + push({ + id: "g1.engineNeverInitialized", + grid: "g1", + kind: "obs", + processStarted: null, + landed: null, + expectLanded: null, + ok: false, + detail: { health: await health(), mainLines: identity.fenceMainLines, serverLines: identity.fenceServerLines, appStderrTail: appLog.join("").slice(-4000) }, + }) + throw new Error(`engine never initialized within ${INIT_TIMEOUT}ms — see g1.engineNeverInitialized`) + } + identity.engineUrl = init.url + auth = init.username || init.password ? "Basic " + Buffer.from(`${init.username ?? ""}:${init.password ?? ""}`).toString("base64") : undefined + const agents = await api("GET", "/agent", DEFAULT_WS) + agentName = (Array.isArray(agents.body) && agents.body.find((a: any) => a?.name === "build")?.name) || (Array.isArray(agents.body) ? agents.body[0]?.name : undefined) || "build" + identity.agent = agentName + + // ---- 布局自证 + sidecar env 里的代理栈(必须是 main 改写后的那份,不是 runner 的) + const envProbe = await shellIn(DEFAULT_WS, `echo ${MARK}; echo "HOME=$HOME"; echo "TMPDIR=$TMPDIR"; echo "XDG_CONFIG_HOME=$XDG_CONFIG_HOME"; echo "HTTPS_PROXY=$HTTPS_PROXY"; echo "HTTP_PROXY=$HTTP_PROXY"; echo "ALL_PROXY=$ALL_PROXY"; echo "NO_PROXY=$NO_PROXY"; echo "no_proxy=$no_proxy"`) + const seen = (k: string) => envProbe.output.match(new RegExp(`^${k}=(.*)$`, "m"))?.[1] ?? "" + const realTmpResolved = realpathSync(REAL_TMPDIR) + const seenTmp = seen("TMPDIR") + push({ + id: "layout.rootsOutsideW11W12+sidecarEnv", + grid: "g0", + kind: "identity", + processStarted: envProbe.output.includes(MARK), + landed: null, + expectLanded: null, + ok: + ROOT.startsWith(ISO) && + !ROOT.startsWith("/private/var/folders") && + !ROOT.startsWith("/private/tmp") && + existsSync(logsDir()) && + existsSync(DEFAULT_WS) && + seen("HOME") === HOME && + (seenTmp.replace(/\/$/, "") === REAL_TMPDIR.replace(/\/$/, "") || (!!seenTmp && realpathSync(seenTmp) === realTmpResolved)) && + seen("XDG_CONFIG_HOME") === XDG_CONFIG && + /packaged:\s*true/.test(String(identity.appStartingLine)), + detail: { root: ROOT, sidecarSees: { HOME: seen("HOME"), TMPDIR: seenTmp, XDG_CONFIG_HOME: seen("XDG_CONFIG_HOME") }, appStartingLine: identity.appStartingLine }, + }) + push({ + id: "g1.sidecarEnv.proxyStackRewrittenByMain", + grid: "g1", + kind: "net", + processStarted: envProbe.output.includes(MARK), + landed: null, + expectLanded: null, + ok: + envProbe.output.includes(MARK) && + proxyPort > 0 && + seen("HTTPS_PROXY") === `http://127.0.0.1:${proxyPort}` && + seen("HTTP_PROXY") === `http://127.0.0.1:${proxyPort}` && + seen("ALL_PROXY") === `http://127.0.0.1:${proxyPort}` && + seen("NO_PROXY") === "127.0.0.1,localhost,::1" && + seen("no_proxy") === "127.0.0.1,localhost,::1", + detail: { HTTPS_PROXY: seen("HTTPS_PROXY"), HTTP_PROXY: seen("HTTP_PROXY"), ALL_PROXY: seen("ALL_PROXY"), NO_PROXY: seen("NO_PROXY"), no_proxy: seen("no_proxy"), runnerHadNoProxyEnv: !("HTTPS_PROXY" in env) }, + }) + + // ============================================================ 格 1:冷启动 + let h = 0 + for (let i = 0; i < 60 && h !== 200; i++) { + h = await health() + if (h !== 200) await sleep(1000) + } + push({ id: "g1.coldStart.health200", grid: "g1", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: h === 200, detail: { health: h } }) + const cfg = await api("GET", "/config", DEFAULT_WS) + push({ id: "g1.coldStart.configReadable", grid: "g1", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: cfg.status === 200, detail: { status: cfg.status, shell: cfg.body?.shell ?? null } }) + + // ---- 引擎自己的出网:provider 列表 + 模型目录缓存(观测) + const providers = await api("GET", "/config/providers", DEFAULT_WS, undefined, 90_000) + const provList: any[] = Array.isArray(providers.body?.providers) ? providers.body.providers : Array.isArray(providers.body) ? providers.body : [] + push({ + id: "gN.modelsDev.providerListSize", + grid: "gN", + kind: "net", + processStarted: null, + landed: null, + expectLanded: null, + ok: null, + detail: { status: providers.status, providerCount: provList.length, providerIds: provList.map((p: any) => p?.id).filter(Boolean).slice(0, 40), modelsCacheExists: existsSync(join(XDG_CACHE, "opencode", "models.json")), egressSoFar: egressByTarget(egressRecords()) }, + }) + + // ============================================================ 格 2:装一个连接器(MCP stdio)+ 两处 provider 目录(W7 静默失败路径 —— AC3 的硬判据) + const NODE = run("/usr/bin/which", ["node"]).stdout.trim() || "node" + const mcp = await api("POST", "/mcp", DEFAULT_WS, { name: "ac1337probe", config: { type: "local", command: [NODE, MCP_PROBE, DEFAULT_WS, ESC, ARM], enabled: true } }, 120_000) + await sleep(2000) + const mcpConnected = JSON.stringify(mcp.body).includes("connected") + push({ id: "g2.mcp.connected", grid: "g2", kind: "obs", processStarted: mcpConnected, landed: null, expectLanded: null, ok: mcp.status === 200 && mcpConnected, detail: { status: mcp.status, body: mcp.body } }) + probeFile("g2.mcp.inside(W1 default ws)", "g2", join(DEFAULT_WS, `mcp-${ARM}.txt`), true, mcpConnected) + probeFile("g2.mcp.escape", "g2", join(ESC, `mcp-${ARM}.txt`), false, mcpConnected) + const providerDirs = [join(XDG_CONFIG, "opencode", "node_modules"), join(USER_DATA, "alpha-engine-config", "node_modules")] + const installed = (d: string) => existsSync(join(d, "@opencode-ai", "plugin", "package.json")) + for (let i = 0; i < 180 && !providerDirs.every(installed); i++) await sleep(1000) + const npmFail = grepLog(readEngineLogs(), /background dependency install failed|NpmInstallFailedError|EPERM|ENOTFOUND|ECONNREFUSED|403/i, 30) + const dirReport = providerDirs.map((d) => ({ dir: d, pluginInstalled: installed(d), topLevel: existsSync(d) ? readdirSync(d).filter((f) => !f.startsWith(".")).length : 0, sizeKB: existsSync(d) ? Number(run("/usr/bin/du", ["-sk", d]).stdout.split("\t")[0]) : 0 })) + const npmRecords = egressRecords().filter((r) => r.host === "registry.npmjs.org") + push({ + id: "g2.providerInstall.bothDirsPopulated(W6/W3)+npmCache(W7)+viaGate", + grid: "g2", + kind: "net", + processStarted: null, + landed: providerDirs.every(installed), + expectLanded: true, + // AC3 硬判据:两处目录都装出包(实读 @opencode-ai/plugin 在场 + 顶层条目数 > 0)、~/.npm/_cacache 在、且 registry.npmjs.org 的隧道记录在 main.log 里 + ok: providerDirs.every(installed) && dirReport.every((d) => d.topLevel > 0) && existsSync(join(HOME, ".npm", "_cacache")) && npmRecords.some((r) => r.verdict === "allow"), + detail: { dirs: dirReport, npmCacache: existsSync(join(HOME, ".npm", "_cacache")), npmFailLines: npmFail, npmGateRecords: { allow: npmRecords.filter((r) => r.verdict === "allow").length, deny: npmRecords.filter((r) => r.verdict === "deny").length } }, + }) + + // ============================================================ 格 3:开终端 + const ptyDefault = await api("POST", "/pty", DEFAULT_WS, { cwd: DEFAULT_WS, title: "ac1337" }) + const ptyID = ptyDefault.body?.id + push({ id: "g3.pty.default200(W15)", grid: "g3", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: ptyDefault.status === 200 && !!ptyID, detail: { status: ptyDefault.status, body: ptyDefault.body } }) + let ptyOut = "" + if (ptyID) { + const ptyTicket = await api("POST", `/pty/${ptyID}/connect-token`, DEFAULT_WS, undefined, 30_000, { "x-opencode-ticket": "1" }) + const ticket = ptyTicket.body?.ticket + if (ticket) { + const wsUrl = new URL(String(init.url).replace(/^http/, "ws") + `/pty/${ptyID}/connect`) + wsUrl.searchParams.set("directory", DEFAULT_WS) + wsUrl.searchParams.set("ticket", ticket) + const ws = new WebSocket(wsUrl) + ws.binaryType = "arraybuffer" + const chunks: string[] = [] + ws.onmessage = (ev) => { + const d = ev.data + if (typeof d === "string") chunks.push(d) + else if (d instanceof ArrayBuffer) { + const u = new Uint8Array(d) + chunks.push(u[0] === 0 ? `` : new TextDecoder().decode(u)) + } + } + await new Promise((res, rej) => { + ws.onopen = () => res() + ws.onerror = () => rej(new Error("pty ws error")) + setTimeout(() => rej(new Error("pty ws open timeout")), 15_000) + }).catch((e) => chunks.push(``)) + await sleep(2500) + ws.send(`echo ${MARK}; echo x > ${q(join(DEFAULT_WS, `pty-default-${ARM}.txt`))}; echo x > ${q(join(ESC, `pty-default-${ARM}.txt`))}; echo "PTY_HTTPS_PROXY=$HTTPS_PROXY"; echo AC1337-PTY-DONE\r`) + await sleep(3500) + ws.send(`exit\r`) + await sleep(1500) + ptyOut = chunks.join("") + try { + ws.close() + } catch {} + } + await api("DELETE", `/pty/${ptyID}`, DEFAULT_WS, undefined, 15_000).catch(() => undefined) + } + const ptyStarted = ptyOut.includes(MARK) + push({ id: "g3.pty.defaultLoginShellRanACommand", grid: "g3", kind: "obs", processStarted: ptyStarted, landed: null, expectLanded: null, ok: ptyStarted && ptyOut.includes("AC1337-PTY-DONE"), detail: { output: ptyOut.slice(-1500), ptyHttpsProxy: ptyOut.match(/PTY_HTTPS_PROXY=(http\S*)/)?.[1] ?? null } }) + probeFile("g3.pty.default.inside", "g3", join(DEFAULT_WS, `pty-default-${ARM}.txt`), true, ptyStarted) + probeFile("g3.pty.default.escape", "g3", join(ESC, `pty-default-${ARM}.txt`), false, ptyStarted) + + // ============================================================ 格 4:shell 工具 + const sh = await shellIn( + DEFAULT_WS, + `echo ${MARK}; echo x > ${q(join(DEFAULT_WS, `shelltool-${ARM}.txt`))}; echo x > ${q(join(ESC, `shelltool-${ARM}.txt`))}; echo x > ${q(join(LAUNCH_TMP, `outside-root-${ARM}.txt`))}; echo done`, + ) + const shStarted = sh.output.includes(MARK) + push({ id: "g4.shellTool.ranToCompletion", grid: "g4", kind: "obs", processStarted: shStarted, landed: null, expectLanded: null, ok: sh.status === 200 && shStarted && sh.output.includes("done") && !sh.output.includes("sandbox_apply"), detail: { status: sh.status, output: sh.output.slice(0, 800) } }) + probeFile("g4.shellTool.inside(W1)", "g4", join(DEFAULT_WS, `shelltool-${ARM}.txt`), true, shStarted) + probeFile("g4.shellTool.escape", "g4", join(ESC, `shelltool-${ARM}.txt`), false, shStarted) + probeFile("g4.shellTool.escape.siblingOfOnboardingRoot", "g4", join(LAUNCH_TMP, `outside-root-${ARM}.txt`), false, shStarted) + + // ============================================================ 格 5:写项目配置 + const alphaJsonc = join(ALPHA_GLOBAL_ROOT, "alpha.jsonc") + const engineCfgDir = join(USER_DATA, "alpha-engine-config") + const injectionLines = grepLog(readLog("main.log"), /injection/i) + push({ + id: "g5.injection.alphaGlobalRoot(W2)+engineConfig(W3)", + grid: "g5", + kind: "inside", + path: alphaJsonc, + processStarted: null, + landed: existsSync(alphaJsonc), + expectLanded: true, + ok: existsSync(alphaJsonc) && existsSync(engineCfgDir) && !injectionLines.some((l) => /fail|EPERM/i.test(l)), + detail: { alphaJsoncMtimeAfterLaunch: existsSync(alphaJsonc) ? statSync(alphaJsonc).mtimeMs >= launchedAt : null, engineConfigEntries: existsSync(engineCfgDir) ? readdirSync(engineCfgDir).sort() : null, injectionLines }, + }) + const toolIds = await api("GET", "/experimental/tool/ids", DEFAULT_WS) + const ids: string[] = Array.isArray(toolIds.body) ? toolIds.body : [] + push({ id: "g5.extLoadedInSidecar(alpha_register present)", grid: "g5", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: toolIds.status === 200 && ids.includes("alpha_register"), detail: { status: toolIds.status, alphaTools: ids.filter((i) => i.startsWith("alpha_")) } }) + + // ============================================================ 格 6:多工作区(启动时并集,经产品 store IPC + crash self-heal)—— 顺带验代理跨代同一端口 + const tabs = JSON.stringify([ + { type: "draft", draftID: "ac1337-a", server: "sidecar", directory: WS_A }, + { type: "draft", draftID: "ac1337-b", server: "sidecar", directory: WS_B }, + ]) + await cdp.eval(`window.api.storeSet("opencode.global.dat","tabs",${JSON.stringify(tabs)})`) + await cdp.eval(`window.api.storeSet("opencode.global.dat","tabs.recent",${JSON.stringify(JSON.stringify({ key: "draft:ac1337-a" }))})`) + const gen1Pids = sidecarPids() + for (const pid of gen1Pids) { + try { + process.kill(pid, "SIGKILL") + } catch {} + } + let downSeen = false + for (let i = 0; i < 60; i++) { + const s = await health() + if (s !== 200) downSeen = true + if (downSeen && s === 200) break + await sleep(500) + } + await sleep(2500) + const mainLog2 = readLog("main.log") + const plannedAll = grepLog(mainLog2, /process fence planned/) + const planned2 = plannedAll[1] ?? "" + const applied2 = grepLog(readLog("server.log"), /process fence applied/) + const proxyLinesAll = grepLog(mainLog2, /network egress policy proxy listening on/) + push({ + id: "g6.union.selfHealRespawnRePlannedThree+sameProxyPort", + grid: "g6", + kind: "obs", + processStarted: gen1Pids.length > 0 && downSeen, + landed: null, + expectLanded: null, + ok: gen1Pids.length > 0 && downSeen && (await health()) === 200 && /workspaces=3 \(candidates=3/.test(planned2) && applied2.length >= 2 && planned2.includes(`egressProxyPort=${proxyPort}`) && proxyLinesAll.length === 1, + detail: { gen1Pids, plannedAll, appliedAll: applied2, profileBytesGen2: planned2.match(/profile=(\d+)B/)?.[1] ?? null, proxyListeningLines: proxyLinesAll.length }, + }) + const multiCmd = `echo ${MARK}; echo x > ${q(join(WS_B, `multi-${ARM}.txt`))}; echo x > ${q(join(WS_A, `multi-${ARM}.txt`))}; echo x > ${q(join(DEFAULT_WS, `multi-${ARM}.txt`))}; echo x > ${q(join(WS_C, `multi-${ARM}.txt`))}; echo done` + const multiT0 = Date.now() + let multi = await shellIn(WS_B, multiCmd) + const multiFirstMs = Date.now() - multiT0 + if (!multi.output.includes(MARK)) { + await sleep(5000) + const retry = await shellIn(WS_B, multiCmd) + push({ id: "g6.multi.firstAttemptEmpty(retried)", grid: "g6", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: null, detail: { firstStatus: multi.status, firstMs: multiFirstMs, firstRaw: JSON.stringify(multi.raw).slice(0, 700), retryStatus: retry.status, retryRaw: JSON.stringify(retry.raw).slice(0, 700) } }) + multi = retry + } + const multiStarted = multi.output.includes(MARK) + probeFile("g6.union.wsB.inside", "g6", join(WS_B, `multi-${ARM}.txt`), true, multiStarted, { status: multi.status, firstAttemptMs: multiFirstMs, output: multi.output.slice(0, 400) }) + probeFile("g6.union.wsA.inside", "g6", join(WS_A, `multi-${ARM}.txt`), true, multiStarted) + probeFile("g6.union.defaultWorkspace.inside", "g6", join(DEFAULT_WS, `multi-${ARM}.txt`), true, multiStarted) + probeFile("g6.union.wsC.outsideStore", "g6", join(WS_C, `multi-${ARM}.txt`), false, multiStarted, { output: multi.output.slice(0, 600) }) + + // ============================================================ AC3:误伤语料(文件轴 §2.8 的九条 + 本轴三条;example.com 按 #1073 裁决换成注册表里的真实开发目的地) + const repoProbeFile = join(REPO, "package.json") + const benign: Array<[string, string, (o: string) => { ok: boolean | null; detail: unknown }]> = [ + ["workspace write (WORKDIR)", `echo ${MARK}; echo ok > inside.txt; ls inside.txt`, (o) => ({ ok: o.includes(MARK) && existsSync(join(DEFAULT_WS, "inside.txt")), detail: {} })], + ["/private/tmp write", `echo ${MARK}; echo ok > /private/tmp/ac1337-tmp-probe-${ARM}.txt`, (o) => ({ ok: o.includes(MARK) && existsSync(`/private/tmp/ac1337-tmp-probe-${ARM}.txt`), detail: {} })], + [ + "git init + commit", + `echo ${MARK}; mkdir -p fp-git && cd fp-git && git init -q . && echo hi > a.txt && git add a.txt && git -c user.email=ac1337@example.com -c user.name=ac1337 commit -q -m probe && git rev-parse HEAD`, + (o) => ({ ok: o.includes(MARK) && existsSync(join(DEFAULT_WS, "fp-git", ".git")) && /\b[0-9a-f]{40}\b/.test(o), detail: { sha40: /\b[0-9a-f]{40}\b/.test(o) } }), + ], + [ + "node writes TMPDIR", + `echo ${MARK}; node -e 'const os=require("os"),fs=require("fs"),p=os.tmpdir()+"/ac1337-node-"+process.pid+".txt";fs.writeFileSync(p,"ok");console.log("NODEWROTE="+p)'`, + (o) => { + const m = o.match(/NODEWROTE=(\S+)/) + return { ok: o.includes(MARK) && !!m && existsSync(m[1]), detail: { path: m?.[1] } } + }, + ], + ["read repo file", `echo ${MARK}; head -c 40 ${q(repoProbeFile)}`, (o) => ({ ok: o.includes(MARK) && o.includes("{"), detail: {} })], + [ + // #1073 裁决一:example.com 不进注册表;语料换成注册表里已有的真实开发目的地(registry.npmjs.org:443,§2.2 E13) + "curl https://registry.npmjs.org (registered)", + `echo ${MARK}; curl -sS -o /dev/null -m 25 -w 'CURL=%{http_code}' https://registry.npmjs.org/semver/latest; echo " curl_exit=$?"`, + (o) => ({ ok: o.includes(MARK) && /CURL=200/.test(o), detail: { code: o.match(/CURL=(\d{3})/)?.[1] ?? "none", tail: o.slice(-300) } }), + ], + ["mkdir -p deep", `echo ${MARK}; mkdir -p d1/d2/d3/d4 && test -d d1/d2/d3/d4 && echo MKDIR-OK`, (o) => ({ ok: o.includes(MARK) && o.includes("MKDIR-OK") && existsSync(join(DEFAULT_WS, "d1/d2/d3/d4")), detail: {} })], + ["grep", `echo ${MARK}; printf 'aaa\\nbbb\\n' > g.txt && grep -c bbb g.txt`, (o) => ({ ok: o.includes(MARK) && /^\s*1\s*$/m.test(o), detail: {} })], + ["which git node", `echo ${MARK}; which git node`, (o) => ({ ok: o.includes(MARK) && /\/git/.test(o) && /node/.test(o), detail: { out: o.slice(0, 200) } })], + ["npm view (registry)", `echo ${MARK}; npm view semver version --registry=https://registry.npmjs.org/ 2>&1 | tail -3; echo "npm_exit=$?"`, (o) => ({ ok: o.includes(MARK) && /^\s*\d+\.\d+\.\d+\s*$/m.test(o), detail: { tail: o.slice(-400) } })], + ["git ls-remote (https github.com)", `echo ${MARK}; git ls-remote --exit-code https://github.com/git/git HEAD 2>&1 | tail -2; echo "git_exit=$?"`, (o) => ({ ok: o.includes(MARK) && /\b[0-9a-f]{40}\b/.test(o), detail: { tail: o.slice(-400) } })], + // DNS 仍然不通(解析搬到了代理那一侧,#1334 Q3):这是预期形态,不是误伤 + ["dns only (expected ENOTFOUND)", `echo ${MARK}; node -e 'require("dns").lookup("example.com",(e,a)=>console.log("DNS="+(e?e.code:a)))'`, (o) => ({ ok: o.includes(MARK) && /DNS=(ENOTFOUND|EAI_AGAIN|EAI_NONAME)/.test(o), detail: { dns: o.match(/DNS=(\S+)/)?.[1] ?? "none" } })], + ] + for (const [tag, command, judge] of benign) { + const r = await shellIn(DEFAULT_WS, command, 90_000) + const j = judge(r.output) + push({ id: `ac3.benign/${tag}`, grid: "ac3", kind: "benign", processStarted: r.output.includes(MARK), landed: null, expectLanded: null, ok: j.ok === null ? null : j.ok && r.output.includes(MARK), detail: { status: r.status, ...(j.detail as object), output: r.output.slice(0, 500) } }) + } + + // ============================================================ AC1 反臂:逃逸语料(每条自带进程已启动的证据;绕代理直连 / raw-IP:443 / UDP / [::1] 其它端口 / 未登记目的地) + const closedV6Port = await freePort() + const escapes: Array<[string, string, (o: string) => { ok: boolean; detail: unknown }]> = [ + [ + "unregistered destination via gate (curl https://example.com)", + // curl 对 CONNECT 被 403 的报法是 `(56) CONNECT tunnel failed, response 403`,`-w %{http_code}` 那一格是 000 —— 读 curl 自己的话,不读 -w + `echo ${MARK}; curl -sS -o /dev/null -m 25 -w 'CURL=%{http_code}' https://example.com; echo " curl_exit=$?"`, + (o) => { + const deny = egressRecords().filter((r) => r.host === "example.com" && r.verdict === "deny" && r.reason === "unregistered") + // 判据 = curl 自己的错误文本 + 代理侧 deny 记录;`$?` 经引擎 shell 工具读回恒 0(两轮实测,未追根),只记不判 + return { ok: /curl: \(56\) CONNECT tunnel failed, response 403/.test(o) && !/CURL=200/.test(o) && deny.length >= 1, detail: { curlExitAsSeen: o.match(/curl_exit=(\d+)/)?.[1] ?? "none", gateDenyRecords: deny.slice(-1), tail: o.slice(-200) } } + }, + ], + [ + "bypass proxy, raw-IP:443 (curl --noproxy '*' https://1.1.1.1)", + `echo ${MARK}; curl --noproxy '*' -sS -o /dev/null -m 10 -w 'CURL=%{http_code}' https://1.1.1.1/; echo " curl_exit=$?"`, + // (7) Failed to connect … after 0 ms:内核 EPERM,不是超时 + (o) => ({ ok: /curl: \(7\) Failed to connect to 1\.1\.1\.1 port 443/.test(o) && !/CURL=200/.test(o), detail: { curlExitAsSeen: o.match(/curl_exit=(\d+)/)?.[1] ?? "none", afterMs: o.match(/after (\d+) ms/)?.[1] ?? null, tail: o.slice(-200) } }), + ], + [ + "bypass proxy, by name (curl --noproxy '*' https://github.com)", + `echo ${MARK}; curl --noproxy '*' -sS -o /dev/null -m 10 -w 'CURL=%{http_code}' https://github.com/; echo " curl_exit=$?"`, + // (6) Could not resolve host:绕开代理 ⇒ 死在 DNS 这一步(#1334 Q3),不是连上了 + (o) => ({ ok: /curl: \(6\) Could not resolve host: github\.com/.test(o) && !/CURL=200/.test(o), detail: { curlExitAsSeen: o.match(/curl_exit=(\d+)/)?.[1] ?? "none", tail: o.slice(-200) } }), + ], + [ + "raw TCP to 1.1.1.1:443 (node net.connect)", + `echo ${MARK}; node -e 'const s=require("net").connect(443,"1.1.1.1");s.on("connect",()=>{console.log("TCP=CONNECTED");s.destroy()});s.on("error",e=>console.log("TCP="+e.code))'`, + (o) => ({ ok: /TCP=EPERM/.test(o), detail: { tcp: o.match(/TCP=(\S+)/)?.[1] ?? "none" } }), + ], + [ + "UDP sendto 1.1.1.1:53 (node dgram)", + `echo ${MARK}; node -e 'const s=require("dgram").createSocket("udp4");s.send(Buffer.from("x"),53,"1.1.1.1",e=>{console.log("UDP="+(e?e.code:"SENT"));s.close()})'`, + (o) => ({ ok: /UDP=EPERM/.test(o), detail: { udp: o.match(/UDP=(\S+)/)?.[1] ?? "none" } }), + ], + [ + "[::1] other port (node net.connect)", + `echo ${MARK}; node -e 'const s=require("net").connect({host:"::1",port:${closedV6Port}});s.on("connect",()=>{console.log("V6=CONNECTED");s.destroy()});s.on("error",e=>console.log("V6="+e.code))'`, + (o) => ({ ok: /V6=EPERM/.test(o), detail: { v6: o.match(/V6=(\S+)/)?.[1] ?? "none", port: closedV6Port } }), + ], + ] + for (const [tag, command, judge] of escapes) { + const r = await shellIn(DEFAULT_WS, command, 90_000) + const started = r.output.includes(MARK) + const j = judge(r.output) + push({ id: `ac1.escape/${tag}`, grid: "ac1", kind: "netEscape", processStarted: started, landed: null, expectLanded: null, ok: started && j.ok, detail: { status: r.status, ...(j.detail as object), output: r.output.slice(0, 400) } }) + } + + // ============================================================ 引擎**进程内**的出网经那扇门:远程 MCP 指向未登记的名字 ⇒ 代理 403 unregistered(不是引擎侧 ENOTFOUND) + for (const [scheme, name] of [["http", "ac1337remotehttp"], ["https", "ac1337remotehttps"]] as const) { + const remoteUrl = `${scheme}://ac1337-probe.invalid/mcp` + const before = egressRecords().length + const remote = await api("POST", "/mcp", DEFAULT_WS, { name, config: { type: "remote", url: remoteUrl, enabled: true } }, 90_000) + await sleep(4000) + const gateSaw = egressRecords() + .slice(before) + .filter((r) => r.host === "ac1337-probe.invalid") + push({ + id: `ac1.engineFetch.remoteMcp.${scheme}.viaGate`, + grid: "ac1", + kind: "net", + processStarted: null, + landed: null, + expectLanded: null, + ok: gateSaw.length >= 1 && gateSaw.every((r) => r.verdict === "deny" && r.reason === "unregistered"), + detail: { url: remoteUrl, status: remote.status, body: JSON.stringify(remote.body).slice(0, 500), gateRecords: gateSaw }, + }) + } + + // ---- 时序观测(#1334 §4 的两条,只记不判;本轮只跑一次,不构成结论) + const timeline = grepLog(readLog("main.log"), /catalog_ready|catalog_liveness\.confirmed/, 20) + push({ id: "obs.catalogTimeline", grid: "obs", kind: "obs", processStarted: null, landed: null, expectLanded: null, ok: null, detail: { lines: timeline, g6FirstShellMs: multiFirstMs } }) +} catch (error) { + push({ id: "runner.fatal", grid: "g0", kind: "identity", processStarted: null, landed: null, expectLanded: null, ok: false, detail: String(error) }) +} finally { + try { + cdp?.close() + } catch {} + try { + child?.kill("SIGTERM") + } catch {} + for (let i = 0; i < 20 && child && child.exitCode === null; i++) await sleep(250) + if (child && child.exitCode === null) { + try { + child.kill("SIGKILL") + } catch {} + } + spawnSync("/usr/bin/pkill", ["-9", "-f", `remote-debugging-port=${CDP_PORT}`]) + await sleep(1500) + const residual = psAll().filter((l) => OUR_DIST.test(l) || l.includes(`remote-debugging-port=${CDP_PORT}`)) + const sentinelsAfter = snap() + console.log("REAL HOME sentinels AFTER : " + JSON.stringify(sentinelsAfter)) + // 内核 Sandbox 拒绝行(--last 而不是 --start:log show 的 --start 认本地时区,toISOString 是 UTC,#1334 踩过) + const windowSeconds = Math.ceil((Date.now() - launchedAt) / 1000) + 60 + const denyLines = run("/usr/bin/log", ["show", "--style", "compact", "--last", `${windowSeconds}s`, "--predicate", 'eventMessage CONTAINS "deny("']) + .stdout.split("\n") + .filter((l) => /Sandbox:/.test(l)) + .filter((l) => !/imagent|contactsd|AddressBook|assistantd|Safari|Music|com\.apple\.dt/.test(l)) + .slice(0, 400) + const mainLog = readLog("main.log") + const serverLog = readLog("server.log") + const engineLog = readEngineLogs() + const records = egressRecords() + push({ id: "identity.realHomeUntouched", grid: "g0", kind: "identity", processStarted: null, landed: null, expectLanded: null, ok: JSON.stringify(sentinelsBefore) === JSON.stringify(sentinelsAfter), detail: { before: sentinelsBefore, after: sentinelsAfter } }) + push({ id: "identity.zeroResidualProcesses", grid: "g0", kind: "identity", processStarted: null, landed: null, expectLanded: null, ok: residual.length === 0, detail: { residual } }) + push({ id: "gN.gateLog", grid: "gN", kind: "net", processStarted: null, landed: null, expectLanded: null, ok: null, detail: { records: records.length, byTarget: egressByTarget(records) } }) + const failed = probes.filter((p) => p.ok === false) + const strip = (s: unknown) => JSON.parse(JSON.stringify(s).split(ISO).join("").split(REAL).join("")) + const result = strip({ + ticket: "alpha-code#1337", + arm: ARM, + identity, + escapeDirListing: landedList(ESC), + launchTmpListing: landedList(LAUNCH_TMP), + defaultWsListing: landedList(DEFAULT_WS), + wsAListing: landedList(WS_A), + wsBListing: landedList(WS_B), + wsCListing: landedList(WS_C), + gateRecords: records, + sandboxDenyLines: denyLines, + probes, + summary: { total: probes.length, pass: probes.filter((p) => p.ok === true).length, fail: failed.length, observational: probes.filter((p) => p.ok === null).length }, + logs: { + main: grepLog(mainLog, /process fence|fence plan|injection|app starting|spawning sidecar|sidecar exited|self-heal|respawn|shell env|Loaded shell environment|network egress policy proxy/i, 160), + server: grepLog(serverLog, /process fence|sandbox|fence|EPERM|not permitted|error|proxy|listen/i, 160), + engine: grepLog(engineLog, /dependency install|NpmInstall|EPERM|not permitted|ENOTFOUND|ECONNREFUSED|fetch failed|models\.dev|ac1337|403/i, 120), + appStderrTail: appLog.join("").slice(-4000), + }, + finishedAt: new Date().toISOString(), + }) + mkdirSync(dirname(OUT), { recursive: true }) + writeFileSync(OUT, JSON.stringify(result, null, 2)) + console.log(`\n${ARM}: ${result.summary.pass} pass / ${result.summary.fail} fail / ${result.summary.observational} obs -> ${OUT}`) + console.log(`gate records: ${records.length}; sandbox deny lines captured: ${denyLines.length}`) + console.log(`iso tree ${KEEP ? "kept" : "removed"}: ${ISO}`) + if (!KEEP) { + try { + rmSync(ISO, { recursive: true, force: true }) + } catch {} + } + process.exit(failed.length ? 1 : 0) +} diff --git a/packages/ui-mac/src/main/network-egress-disclosure.test.ts b/packages/ui-mac/src/main/network-egress-disclosure.test.ts new file mode 100644 index 000000000000..d773479b6335 --- /dev/null +++ b/packages/ui-mac/src/main/network-egress-disclosure.test.ts @@ -0,0 +1,107 @@ +// REQ-137 (`#1337`) · AC5 覆盖面如实声明 —— 守住「这道网络围栏只罩引擎那棵树,应用自身的联网不在其内」这段话的 +// **存在与内容**,在两个落点:产品(终端「沙箱开启」悬停卡的第四句,zh / en)与文档(勘破文档 §覆盖面声明)。 +// +// 与 REQ-159 的披露面同法(`#1322`:呈现层用字面量锚点守文案):判据是**独立字面量**(这里手打,不从被守对象派生), +// 缺任一句 ⇒ 红;此外一条**反向**规则 —— 文案若宣称「全部 / 所有出网都被限制」(比实际更大的保护面),必须红。 +// 控制臂:两种已知的坏(删掉排除句 / 改成过度声明)各自经**同一个判据函数**判红并点名。 +// +// 为什么值得一道闸:边界声明是最容易在「润色文案」时被删掉的那种句子,而删掉之后没有任何行为测试会红 —— +// 产品从此宣称一个不存在的保护面(与 REQ-159 AC4「平台边界如实声明」同族)。 + +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { dict as en } from "../renderer/i18n/en" +import { dict as zh } from "../renderer/i18n/zh" + +type Locale = "zh" | "en" | "doc" + +/** 必须逐字在场的片段(手打的独立字面量)。 */ +const REQUIRED: Record = { + zh: [ + "助手和终端只能经 Code Puppy 的出网闸门访问已登记的地址", + "其它地址会被拒绝并留下记录", + "Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内", + ], + en: [ + "the assistant and the terminal can only reach registered addresses through Code Puppy's egress gate", + "anything else is refused and logged", + "Code Puppy's own traffic (model catalog, sign-in, update checks) is outside this sandbox", + ], + doc: ["这道网络围栏只罩引擎 sidecar 那棵进程树", "Electron main", "renderer 的出网不在覆盖内", "不得宣称比这更大的保护面"], +} + +/** 过度声明的形状:「全部 / 所有 … 出网 / 联网 / 网络 … 都被 / 受 …」及其英文。命中即红,不论别的句子在不在。 */ +const OVERCLAIM: Record = { + zh: /(全部|所有|一切)[^。;]{0,12}(出网|联网|网络)[^。;]{0,6}(都|均|全)?(被|受|经)/, + en: /\ball\b[^.;]{0,20}\b(outbound|network|internet)\b[^.;]{0,20}\b(is|are)\b[^.;]{0,12}\b(restricted|blocked|limited|gated|covered)/i, + doc: /(全部|所有|一切)[^。;]{0,12}(出网|联网|网络)[^。;]{0,6}(都|均|全)?(被|受|经)/, +} + +export function judgeEgressCoverageDisclosure(text: string, locale: Locale): { ok: boolean; detail: string } { + const problems: string[] = [] + for (const fragment of REQUIRED[locale]) if (!text.includes(fragment)) problems.push(`missing: ${JSON.stringify(fragment)}`) + const over = OVERCLAIM[locale].exec(text) + if (over) problems.push(`overclaims a larger protection surface than exists: ${JSON.stringify(over[0])}`) + return { ok: problems.length === 0, detail: problems.join("; ") || "coverage statement present and honest" } +} + +const DOC = resolve(import.meta.dir, "../../../../docs/architecture/2026-09-10-network-egress-on-process-fence.md") +const docStatement = () => { + const text = readFileSync(DOC, "utf8") + const m = /([\s\S]*?)/.exec(text) + return m?.[1] ?? "" +} + +describe("REQ-137 #1337 AC5 覆盖面如实声明", () => { + test("产品(终端沙箱悬停卡 zh / en):三句都在,且没有过度声明", () => { + const zhBody = zh["alpha.terminal.sandboxHoverBody"] + const enBody = en["alpha.terminal.sandboxHoverBody"] + expect(typeof zhBody).toBe("string") + expect(typeof enBody).toBe("string") + const zhVerdict = judgeEgressCoverageDisclosure(zhBody, "zh") + const enVerdict = judgeEgressCoverageDisclosure(enBody, "en") + expect(zhVerdict.detail).toBe("coverage statement present and honest") + expect(enVerdict.detail).toBe("coverage statement present and honest") + expect(zhVerdict.ok && enVerdict.ok).toBe(true) + }) + + test("文档(勘破文档 §覆盖面声明,锚点之间):段落在场、四个片段都在、没有过度声明", () => { + const statement = docStatement() + expect(statement.length).toBeGreaterThan(0) + const verdict = judgeEgressCoverageDisclosure(statement, "doc") + expect(verdict.detail).toBe("coverage statement present and honest") + expect(verdict.ok).toBe(true) + }) + + test("反向 ①:把声明改成「全部出网都被限制」⇒ 同一判据必红并点名过度声明(三个落点各一次)", () => { + const zhBad = zh["alpha.terminal.sandboxHoverBody"].replace(/联网也一样[\s\S]*$/, "所有联网都被这道沙箱限制。") + const enBad = en["alpha.terminal.sandboxHoverBody"].replace(/Network access works[\s\S]*$/, "All network traffic is restricted by this sandbox.") + const docBad = docStatement().replace(/\*\*Electron main\*\*[\s\S]*$/, "全部出网都被这道围栏限制。") + for (const [text, locale] of [ + [zhBad, "zh"], + [enBad, "en"], + [docBad, "doc"], + ] as const) { + const verdict = judgeEgressCoverageDisclosure(text, locale) + expect(verdict.ok, locale).toBe(false) + expect(verdict.detail, locale).toMatch(/overclaims a larger protection surface/) + expect(verdict.detail, locale).toMatch(/missing:/) + } + }) + + test("反向 ②:只删掉「应用自身不在其内」那一句(其余照旧)⇒ 红并点名缺的是哪一句", () => { + const zhBad = zh["alpha.terminal.sandboxHoverBody"].replace(";Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内,照常直连", "") + const enBad = en["alpha.terminal.sandboxHoverBody"].replace(" Code Puppy's own traffic (model catalog, sign-in, update checks) is outside this sandbox and connects as before.", "") + expect(zhBad).not.toBe(zh["alpha.terminal.sandboxHoverBody"]) + expect(enBad).not.toBe(en["alpha.terminal.sandboxHoverBody"]) + const zhVerdict = judgeEgressCoverageDisclosure(zhBad, "zh") + const enVerdict = judgeEgressCoverageDisclosure(enBad, "en") + expect(zhVerdict.ok).toBe(false) + expect(zhVerdict.detail).toContain("不在这道沙箱之内") + expect(enVerdict.ok).toBe(false) + expect(enVerdict.detail).toContain("is outside this sandbox") + // 锚点整段被删 ⇒ 文档判据同样红(空串缺全部片段) + expect(judgeEgressCoverageDisclosure("", "doc").ok).toBe(false) + }) +}) diff --git a/packages/ui-mac/src/main/network-egress-fence.test.ts b/packages/ui-mac/src/main/network-egress-fence.test.ts new file mode 100644 index 000000000000..e5d6d8a42676 --- /dev/null +++ b/packages/ui-mac/src/main/network-egress-fence.test.ts @@ -0,0 +1,231 @@ +// REQ-137 (`#1337`) —— AC1 的**强制半场** + AC4:真 .node(生产构建脚本现编)、真 seatbelt、真策略代理 +// (生产 startEgressPolicyProxy)、**Electron 内嵌 node**(= utilityProcess 的运行时),profile 由生产渲染器渲染 +// (`#1334` Q1.2 的四行网络规则 + 可写集),env 由生产 sidecarEgressProxyEnv 给出。 +// darwin-only;CI(ubuntu)上自报 skip,由 gate-files.tsv 按 [平台:darwin] 登记。 +// +// 三臂: +// bare 不套围栏(控制臂):探针必须读得到「通」—— 直连靶站连得上、UDP 发得出、DNS 解得开。 +// 同一个判据函数对这一臂**必须判红**并点名(否则它不是闸门)。 +// fenced/up 围栏 + 代理在场:引擎形态的进程**能 listen 且外面连得进**(票面硬要求一);经 env-proxy 的 fetch +// 与裸 CONNECT 都经代理到达靶站(唯一那扇门);逃逸语料(绕代理直连 / raw-IP:443 / UDP / [::1] 其它端口 +// / DNS)逐条 EPERM 或解析失败 —— 每条自带本进程 pid 与耗时(空输出 ≠ 拦住)。 +// fenced/down 代理**关掉**之后(AC4):同一 profile 下 fetch 与 CONNECT 立刻 ECONNREFUSED 且消息点名代理地址, +// 不是挂到超时;绕代理直连仍 EPERM(不回退直连)。耗时只打印不断言(`#1300`:不许断言机器有多闲)—— +// 「快」由 errno 结构上给出:ECONNREFUSED 是内核对 loopback 上没人听的端口的即时 RST,ETIMEDOUT / 探针超时才是「挂住」。 +// +// 靶站是 loopback 上一个只有代理那一侧解得开的**名字**(fixture dial 把它映到 127.0.0.1:):围栏里 DNS 不通, +// 名字能到达 ⇒ 流量一定是经代理汇出去的,这比「有没有报错」硬。授权函数是测试注入(只放行这一个名字), +// 生产接线(authorize = 注册表)的判据在 network-egress-proxy.test.ts。 + +import { afterAll, beforeAll, describe, expect, test } from "bun:test" +import { spawn } from "node:child_process" +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import * as http from "node:http" +import { createRequire } from "node:module" +import * as net from "node:net" +import { homedir, tmpdir } from "node:os" +import { dirname, join, resolve } from "node:path" +import { buildFenceAddon } from "../../scripts/build-fence-addon" +import { startEgressPolicyProxy, type EgressLogRecord, type EgressProxyHandle } from "./network-egress-proxy" +import { renderProcessFenceProfile, resolveEngineRoots } from "./process-fence-profile" +import { sidecarEgressProxyEnv } from "./sidecar-env" + +const describeDarwin = process.platform === "darwin" && existsSync("/usr/bin/sandbox-exec") ? describe : describe.skip +const require = createRequire(import.meta.url) +const fixtures = resolve(import.meta.dir, "../../test-fixtures/process-fence") +const TARGET_NAME = "ac1337-target.test" + +type Step = { ok: boolean; code?: string | null; message?: string; ms: number; status?: number; body?: string; connected?: boolean; accepted?: number; sent?: boolean; address?: string; head?: string } +type DriverOut = { runtime: string; mode: string; pid: number; buildId?: string; apply?: { rc: number; error: string }; fatal?: string; listenPort?: number; steps: Record } +type DriverRun = { out: DriverOut; served: string; status: number | null } + +describeDarwin("REQ-137 #1337 强制半场 —— 真 .node / 真 seatbelt / 真策略代理 / Electron 的 node", () => { + let scratch = "" + let addon = "" + let buildId = "" + let ws = "" + let profileFile = "" + let electron = "" + let target: http.Server + let targetPort = 0 + let proxy: EgressProxyHandle + let proxyPort = 0 + let closedV6Port = 0 + const proxyLog: EgressLogRecord[] = [] + + beforeAll(async () => { + scratch = mkdtempSync(join(tmpdir(), "ac1337-net-")) + buildId = `ac1337-test-${Date.now()}` + addon = buildFenceAddon({ out: join(scratch, "build", "alpha_fence.node"), buildId }).out + ws = mkdtempSync(join(homedir(), ".ac1337-ws-")) + const userData = join(scratch, "userData") + const globalRoot = join(scratch, "alpha-code-state", "env", "dev") + mkdirSync(userData, { recursive: true }) + mkdirSync(globalRoot, { recursive: true }) + + target = http.createServer((req, res) => res.end(`TARGET-OK host=${req.headers.host ?? ""}`)) + await new Promise((r) => target.listen(0, "127.0.0.1", r)) + targetPort = (target.address() as net.AddressInfo).port + + // 只有代理这一侧解得开的名字:授权只放行它,拨号把它映到靶站(生产 dial 是 net.connect 按名字解析)。 + proxy = await startEgressPolicyProxy({ + log: (record) => void proxyLog.push(record), + authorize: (host, port) => host === TARGET_NAME && port === targetPort, + dial: (host, port) => net.connect({ host: host === TARGET_NAME ? "127.0.0.1" : host, port }), + }) + proxyPort = proxy.port + + const v6 = net.createServer() + await new Promise((r) => v6.listen(0, "::1", r)) + closedV6Port = (v6.address() as net.AddressInfo).port + await new Promise((r) => v6.close(() => r())) + + // 生产渲染的 profile:可写集 = 本测试的 ws + 真 HOME 的 XDG 形状;网络行 = Q1.2 四行,端口 = 上面这个真代理。 + profileFile = join(scratch, "fence.sb") + writeFileSync( + profileFile, + renderProcessFenceProfile({ + workspaces: [ws], + alphaGlobalRoot: globalRoot, + userDataPath: userData, + stateHome: userData, + roots: resolveEngineRoots({}, homedir()), + egressProxyPort: proxyPort, + }), + ) + // 不经 require("electron")(bun 的 mock.module 是进程级的,全量跑时别的文件 mock 掉的 electron 会漏进来); + // 照 electron 包自己的做法从文件系统读 path.txt 再 join dist。 + const electronPkg = dirname(require.resolve("electron/package.json")) + electron = join(electronPkg, "dist", readFileSync(join(electronPkg, "path.txt"), "utf8").trim()) + if (!existsSync(electron)) throw new Error(`electron binary missing at ${electron}(本次测量作废)`) + }) + + afterAll(async () => { + try { + await proxy?.close() + } catch {} + await new Promise((r) => (target ? target.close(() => r()) : r())) + for (const d of [scratch, ws]) { + try { + rmSync(d, { recursive: true, force: true }) + } catch {} + } + }) + + /** 起 driver;看到 `LISTENING ` 就从**本进程**(未被围栏)连进去,证明被围栏的监听者外面连得上。 */ + const runDriver = (mode: "bare" | "fenced", proxyPortForEnv: number): Promise => + new Promise((resolve, reject) => { + const child = spawn(electron, [join(fixtures, "network-driver.mjs"), addon, mode, profileFile, TARGET_NAME, String(targetPort), String(proxyPortForEnv), String(closedV6Port)], { + cwd: ws, + env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", ...sidecarEgressProxyEnv(proxyPortForEnv) }, + }) + let stdout = "" + let stderr = "" + let served = "" + let connected = false + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString() + const m = /^LISTENING (\d+)$/m.exec(stdout) + if (m && !connected) { + connected = true + const sock = net.connect(Number(m[1]), "127.0.0.1", () => sock.write("ping")) + sock.on("data", (d: Buffer) => (served += d.toString())) + sock.on("error", (e) => (served += `ERR ${(e as NodeJS.ErrnoException).code}`)) + } + }) + child.stderr.on("data", (chunk: Buffer) => (stderr += chunk.toString())) + const timer = setTimeout(() => { + child.kill("SIGKILL") + reject(new Error(`driver(${mode}) did not finish within 60s; stdout=${stdout.slice(-800)} stderr=${stderr.slice(-800)}`)) + }, 60_000) + child.on("exit", (status) => { + clearTimeout(timer) + const line = stdout.trim().split("\n").filter((l) => l.startsWith("{")).at(-1) + if (!line) return reject(new Error(`driver(${mode}) printed no JSON (status ${status}); stdout=${stdout.slice(-800)} stderr=${stderr.slice(-800)}`)) + const out = JSON.parse(line) as DriverOut + if (out.fatal) return reject(new Error(`driver(${mode}) fatal: ${out.fatal}`)) + // 给外面那条连接一点时间把 SERVED 收完 + setTimeout(() => resolve({ out, served, status }), 100) + }) + }) + + /** + * 判据本体(AC1 强制半场):唯一那扇门通,其余逃逸语料全部 EPERM / 解析失败。返回 ok + 可读的 detail, + * 好让控制臂(bare)证明它会红。 + */ + const judgeEnforcement = (run: DriverRun): { ok: boolean; detail: string } => { + const s = run.out.steps + const problems: string[] = [] + if (!(run.out.pid > 0)) problems.push("no pid — the probe process did not report starting") + if (!s.listen?.ok || run.served !== "SERVED:ping") problems.push(`listen/inbound: ${JSON.stringify(s.listen)} served=${JSON.stringify(run.served)}`) + if (!(s.proxyFetch?.ok && s.proxyFetch.status === 200 && String(s.proxyFetch.body).startsWith("TARGET-OK"))) problems.push(`proxyFetch not 200 via the gate: ${JSON.stringify(s.proxyFetch)}`) + if (!(s.proxyConnect?.ok && s.proxyConnect.status === 200)) problems.push(`raw CONNECT via the gate not 200: ${JSON.stringify(s.proxyConnect)}`) + for (const name of ["directTcp", "rawIp443", "udp", "v6OtherPort"]) { + const step = s[name] + if (!step || step.ok || step.code !== "EPERM") problems.push(`${name} should be EPERM, got ${JSON.stringify(step)}`) + } + if (!s.dns || s.dns.ok || !s.dns.code) problems.push(`dns should fail (mDNSResponder is behind (deny network*)), got ${JSON.stringify(s.dns)}`) + return { ok: problems.length === 0, detail: problems.join("; ") || "all probes as expected" } + } + + const runs: Record = {} + + test("A0 控制臂(bare):不套围栏时探针读得到「通」—— 直连靶站连得上、UDP 发得出、DNS 解得开、外面连得进监听者", async () => { + runs.bare = await runDriver("bare", proxyPort) + const s = runs.bare.out.steps + expect(runs.bare.out.runtime).toMatch(/^node 24\./) + expect(runs.bare.served).toBe("SERVED:ping") + expect(s.directTcp).toMatchObject({ ok: true, connected: true }) + expect(s.udp).toMatchObject({ ok: true, sent: true }) + expect(s.proxyFetch).toMatchObject({ ok: true, status: 200 }) + expect(s.v6OtherPort).toMatchObject({ ok: false, code: "ECONNREFUSED" }) + console.log(`[ac1337 A0 bare] dns=${JSON.stringify(s.dns)} rawIp443=${JSON.stringify(s.rawIp443)}(观测,不判:取决于本机是否联网)`) + }) + + test("A1 围栏 + 代理在场:apply rc=0、能 listen 且外面连得进(硬要求一)、fetch 与 CONNECT 都经代理到达靶站、逃逸语料逐条 EPERM、DNS 不通", async () => { + runs.fencedUp = await runDriver("fenced", proxyPort) + const run = runs.fencedUp + expect(run.out.apply).toEqual({ rc: 0, error: "" }) + expect(run.out.buildId).toBe(buildId) + const verdict = judgeEnforcement(run) + console.log( + `[ac1337 A1 fenced/up] pid=${run.out.pid} ` + + Object.entries(run.out.steps) + .map(([k, v]) => `${k}=${v.ok ? `ok(${v.status ?? v.connected ?? v.sent ?? v.accepted ?? v.address ?? ""},${v.ms}ms)` : `${v.code}(${v.ms}ms)`}`) + .join(" "), + ) + expect(verdict.detail).toBe("all probes as expected") + expect(verdict.ok).toBe(true) + // 代理侧留下的结构化记录:两条 allow(fetch 的 CONNECT + 裸 CONNECT),目的地就是那个只有代理解得开的名字 + const allows = proxyLog.filter((r) => r.event === "egress.connect" && r.verdict === "allow" && r.host === TARGET_NAME && r.port === targetPort) + expect(allows.length).toBeGreaterThanOrEqual(2) + }) + + test("A2 判据测得出已知的坏:同一个判据函数对 bare 臂必须判红,并点名直连连上了", () => { + const verdict = judgeEnforcement(runs.bare!) + expect(verdict.ok).toBe(false) + expect(verdict.detail).toMatch(/directTcp should be EPERM/) + expect(verdict.detail).toMatch(/udp should be EPERM/) + }) + + test("A3 AC4 代理关掉之后:fetch 与 CONNECT 立刻 ECONNREFUSED 且点名代理地址(不是挂到超时);绕代理直连仍 EPERM(不回退)", async () => { + const deadPort = proxyPort + await proxy.close() + runs.fencedDown = await runDriver("fenced", deadPort) + const s = runs.fencedDown.out.steps + console.log(`[ac1337 A3 fenced/down] proxyFetch=${JSON.stringify(s.proxyFetch)} proxyConnect=${JSON.stringify(s.proxyConnect)} directTcp=${JSON.stringify(s.directTcp)}`) + expect(runs.fencedDown.out.apply).toEqual({ rc: 0, error: "" }) + expect(s.proxyFetch.ok).toBe(false) + expect(s.proxyFetch.code).toBe("ECONNREFUSED") + expect(s.proxyFetch.message).toContain(`127.0.0.1:${deadPort}`) + expect(s.proxyConnect.ok).toBe(false) + expect(s.proxyConnect.code).toBe("ECONNREFUSED") + // 「快」的结构性判据:不是探针超时、不是 ETIMEDOUT + for (const name of ["proxyFetch", "proxyConnect"]) expect(s[name].code, name).not.toMatch(/PROBE_TIMEOUT|ETIMEDOUT/) + expect(s.directTcp).toMatchObject({ ok: false, code: "EPERM" }) + expect(s.rawIp443).toMatchObject({ ok: false, code: "EPERM" }) + expect(s.dns.ok).toBe(false) + // 监听不受代理死活影响(引擎照常起得来) + expect(runs.fencedDown.served).toBe("SERVED:ping") + }) +}) diff --git a/packages/ui-mac/src/main/network-egress-proxy.test.ts b/packages/ui-mac/src/main/network-egress-proxy.test.ts index 954571f2de26..aee7e13f19d7 100644 --- a/packages/ui-mac/src/main/network-egress-proxy.test.ts +++ b/packages/ui-mac/src/main/network-egress-proxy.test.ts @@ -175,7 +175,7 @@ describe("AC1 策略半场:授权目的地经隧道转发", () => { expect(closed).toMatchObject({ event: "egress.tunnel-closed", host: "localhost", port: target.port, bytesUp: "hello-through-tunnel".length, bytesDown: "echo:hello-through-tunnel".length }) }) - test("默认 authorize 就是注册表:不传 authorize 时 127.0.0.1:11434(登记在案)走到拨号,未登记端口走不到", async () => { + test("默认 authorize 就是注册表:不传 authorize 时 github.com:443(登记在案)走到拨号,同 host 别的端口走不到", async () => { const dialed: string[] = [] const { proxy, logs } = await startProxy({ dial: (h, p) => { @@ -185,12 +185,12 @@ describe("AC1 策略半场:授权目的地经隧道转发", () => { return s }, }) - const registered = await rawRequest(proxy.port, "CONNECT 127.0.0.1:11434 HTTP/1.1\r\nHost: 127.0.0.1:11434\r\n\r\n") - const unregistered = await rawRequest(proxy.port, "CONNECT 127.0.0.1:11435 HTTP/1.1\r\nHost: 127.0.0.1:11435\r\n\r\n") - expect(dialed).toEqual(["127.0.0.1:11434"]) + const registered = await rawRequest(proxy.port, "CONNECT github.com:443 HTTP/1.1\r\nHost: github.com:443\r\n\r\n") + const unregistered = await rawRequest(proxy.port, "CONNECT github.com:22 HTTP/1.1\r\nHost: github.com:22\r\n\r\n") + expect(dialed).toEqual(["github.com:443"]) expect(registered.status).toBe(502) expect(unregistered.status).toBe(403) - expect(logs.map((r) => (r.event === "egress.connect" ? `${r.authority}:${r.verdict}:${r.reason ?? "-"}` : r.event))).toEqual(["127.0.0.1:11434:deny:dial-failed", "127.0.0.1:11435:deny:unregistered"]) + expect(logs.map((r) => (r.event === "egress.connect" ? `${r.authority}:${r.verdict}:${r.reason ?? "-"}` : r.event))).toEqual(["github.com:443:deny:dial-failed", "github.com:22:deny:unregistered"]) }) }) diff --git a/packages/ui-mac/src/main/network-egress-registry.test.ts b/packages/ui-mac/src/main/network-egress-registry.test.ts index f5b701019543..46d0a321d965 100644 --- a/packages/ui-mac/src/main/network-egress-registry.test.ts +++ b/packages/ui-mac/src/main/network-egress-registry.test.ts @@ -11,7 +11,6 @@ import { EGRESS_REGISTRY, egressKey, isEgressAuthorized } from "./network-egress /** 勘破 §2.2 的清单,按今日代码坐标校正(见注册表抬头逐条说明)。独立锚,不 import 被测对象。 */ const EXPECTED_INITIAL_SET = [ - "127.0.0.1:11434", "account.codepuppy.cn:443", "alpha-cloud.tidelabs.click:443", "alpha-gateway.tidelabs.click:443", @@ -24,6 +23,8 @@ const EXPECTED_INITIAL_SET = [ "models.opencode.ai:443", "pypi.org:443", "registry.npmjs.org:443", + // `#1337`:#1073 owner 裁决二 —— #1334 Q4 实拍的真实开发流量(shell 工具子进程下载 release 资产) + "release-assets.githubusercontent.com:443", "www.eclipse.org:443", ] @@ -59,7 +60,9 @@ describe("AC2 授权语义(fail-closed)", () => { test("已登记 host:port 放行;同 host 别的端口拒;未登记 host 拒;大小写归一;尾点 / 前导空白 / scheme / 子域一律拒", () => { expect(isEgressAuthorized("registry.npmjs.org", 443)).toBe(true) expect(isEgressAuthorized("REGISTRY.NPMJS.ORG", 443)).toBe(true) - expect(isEgressAuthorized("127.0.0.1", 11434)).toBe(true) + expect(isEgressAuthorized("github.com", 443)).toBe(true) + // owner 2026-09-10 裁决:本机目的地刻意不登记(围栏只放行代理端口,登记也到不了)。 + expect(isEgressAuthorized("127.0.0.1", 11434)).toBe(false) expect(isEgressAuthorized("registry.npmjs.org", 80)).toBe(false) expect(isEgressAuthorized("github.com", 22)).toBe(false) @@ -76,4 +79,15 @@ describe("AC2 授权语义(fail-closed)", () => { expect(isEgressAuthorized("github.com", 65536)).toBe(false) expect(isEgressAuthorized("github.com", 443.5)).toBe(false) }) + +test("本机目的地不得登记(owner 2026-09-10 裁决)—— 围栏只放行代理端口,登记它等于写一句做不到的话", () => { + const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "0.0.0.0", "[::1]"]) + const offenders = EGRESS_REGISTRY.filter((d) => LOOPBACK.has(d.host.toLowerCase())) + expect( + offenders.map((d) => `${d.host}:${d.port}`), + "注册表里出现了 loopback 目的地。围栏只放行 (remote ip \"localhost:<代理端口>\"),别的 loopback 端口一律 EPERM;" + + "且 NO_PROXY 含 loopback ⇒ 这类目的地根本不经代理。登记它不会让它可达,只会让登记簿说假话。" + + "要支持本机目的地请先设计『本机目的地怎么走』,不要往表里加行。", + ).toEqual([]) +}) }) diff --git a/packages/ui-mac/src/main/network-egress-registry.ts b/packages/ui-mac/src/main/network-egress-registry.ts index 6761c57f089d..dcb327ee6db9 100644 --- a/packages/ui-mac/src/main/network-egress-registry.ts +++ b/packages/ui-mac/src/main/network-egress-registry.ts @@ -18,7 +18,10 @@ // (多了 `www.eclipse.org`,server.ts:1207 的 jdtls),按出处如实登记。 // · 与 `#1334` Q4 代理侧实拍逐条对得上:registry.npmjs.org / codepuppy.cn / alpha-gateway.tidelabs.click / // github.com 都在表里。Q4 还拍到 `release-assets.githubusercontent.com:443`(shell 工具子进程)与 -// `example.com:443`(Q3 语料的探针目标)—— **不在 §2.2,本轮不加**;要不要加归 `#1073` 裁,不在这里编。 +// `example.com:443`(Q3 语料的探针目标)—— 两条都不在 §2.2,`#1336` 没自作主张,交 `#1073` 裁。 +// owner 2026-09-10 裁决(`#1073` 评论「注册表初值的三条边界」):前者**加**(真实开发流量:装工具 / 下二进制 +// 都走它,不加则封路之后这类下载被拒 = 误伤,`#1337` 出货复跑再次实拍到它被 403 ×2);后者**不加** +// (它只是探针靶子,不是开发流量;为了让测试过而放宽策略是把闸门做假 —— AC3 语料改用注册表里已有的目的地)。 // // ── 刻意不在表里的 ────────────────────────────────────────────────────────────────── // · BYOK provider 的 baseURL、用户配置的远程 MCP URL:§2.2 列为「动态」类别,没有静态值可登记。 @@ -66,7 +69,14 @@ export const EGRESS_REGISTRY: readonly EgressDestination[] = Object.freeze([ https("download-cdn.jetbrains.com", "子进程:LSP 自动下载", "packages/opencode/src/lsp/server.ts:1330"), https("api.releases.hashicorp.com", "子进程:LSP 自动下载", "packages/opencode/src/lsp/server.ts:1632"), https("www.eclipse.org", "子进程:LSP 自动下载(jdtls)", "packages/opencode/src/lsp/server.ts:1207(§2.2 静态枚举漏列,按出处补)"), - { host: "127.0.0.1", port: 11434, category: "子进程:本机模型(ollama)", source: "§2.2 lsof 实拍" }, + https("release-assets.githubusercontent.com", "子进程:GitHub release 资产下载(装工具 / 下二进制)", "#1334 Q4 choke 臂实拍(shell 工具子进程发起);#1073 owner 裁决二(2026-09-10)"), + // 本机模型(ollama 一类)**刻意不登记** —— owner 2026-09-10 裁决。登记它会写下一句做不到的话: + // 围栏只放行 `(allow network-outbound (remote ip "localhost:<代理端口>"))`,别的 loopback 端口一律 EPERM + // (主 session 实测:围栏下连 127.0.0.1:11434 = EPERM,连放行端口 = ECONNREFUSED,无围栏对照 = CONNECTED); + // 而 NO_PROXY 含 127.0.0.1/localhost/::1 ⇒ 这类目的地本来就不经代理。两边一夹,注册表对它永远不生效。 + // **BYOK 指向 loopback 的 baseURL 撞的是同一堵墙**,且本就属于「动态」类别、静态表里登记不了。 + // 要支持本机目的地,得单独设计「本机目的地怎么走」(放宽围栏?让 loopback 也经代理?),不是往这张表加一行。 + // 判据:network-egress-registry.test.ts 里那条「表内不得出现 loopback 目的地」——有人凭印象加回来即红。 ]) export const egressKey = (host: string, port: number): string => `${host.toLowerCase()}:${port}` diff --git a/packages/ui-mac/src/main/process-fence-apply.test.ts b/packages/ui-mac/src/main/process-fence-apply.test.ts index 0e6299d710b5..f3dfdb5e5694 100644 --- a/packages/ui-mac/src/main/process-fence-apply.test.ts +++ b/packages/ui-mac/src/main/process-fence-apply.test.ts @@ -64,6 +64,8 @@ describeDarwin("REQ-159 process fence —— 真 .node / 真 seatbelt / Electron userDataPath: userData, stateHome: userData, roots: resolveEngineRoots({}, homedir()), + // `#1337`:生产 profile 含网络行;本文件只判文件轴,给一个没人听的端口即可(网络轴判据在 network-egress-fence.test.ts)。 + egressProxyPort: 4443, }) profileFile = join(scratch, "fence.sb") writeFileSync(profileFile, profile) @@ -176,7 +178,7 @@ describeDarwin("REQ-159 process fence —— 真 .node / 真 seatbelt / Electron if (!line) throw new Error(`apply-driver printed nothing (status ${res.status}); stderr: ${res.stderr}`) return { status: res.status, out: JSON.parse(line) as { ok: boolean; name?: string; message?: string; result?: { buildId: string; libsandbox: string; profileBytes: number } } } } - const goodProfile = () => renderProcessFenceProfile({ workspaces: [ws], alphaGlobalRoot: globalRoot, userDataPath: userData, stateHome: userData, roots: resolveEngineRoots({}, homedir()) }) + const goodProfile = () => renderProcessFenceProfile({ workspaces: [ws], alphaGlobalRoot: globalRoot, userDataPath: userData, stateHome: userData, roots: resolveEngineRoots({}, homedir()), egressProxyPort: 4443 }) test("D 正向:生产 profile ⇒ ok,回报 buildId / libsandbox / 字节数,进程退出 0", () => { const r = runApply(goodProfile()) diff --git a/packages/ui-mac/src/main/process-fence-compile.test.ts b/packages/ui-mac/src/main/process-fence-compile.test.ts index 304b244bef1d..9addec3404e8 100644 --- a/packages/ui-mac/src/main/process-fence-compile.test.ts +++ b/packages/ui-mac/src/main/process-fence-compile.test.ts @@ -22,6 +22,8 @@ describeSandbox("trialCompileProfile —— 真 sandbox-exec", () => { userDataPath: join(home, "Library", "Application Support", "ai.opencode.desktop.dev"), stateHome: join(home, "Library", "Application Support", "ai.opencode.desktop.dev"), roots: resolveEngineRoots({}, home), + // `#1337`:网络行也进真编译器(一个没人听的端口就够 —— 这里只编译,不连)。 + egressProxyPort: 4443, } test("[已知的坏 ①] 撞 65535 字节墙的 profile ⇒ ok:false,原因含 `exceeds maximum`", () => { diff --git a/packages/ui-mac/src/main/process-fence-engine.test.ts b/packages/ui-mac/src/main/process-fence-engine.test.ts index d3911154bab1..47a0feffe7c4 100644 --- a/packages/ui-mac/src/main/process-fence-engine.test.ts +++ b/packages/ui-mac/src/main/process-fence-engine.test.ts @@ -28,6 +28,10 @@ import { join, resolve } from "node:path" import { buildFenceAddon } from "../../scripts/build-fence-addon" import { trialCompileProfile } from "./process-fence-compile" import { planProcessFence } from "./process-fence-plan" +// `#1337`:真引擎在围栏下的出网只剩策略代理那一扇门 —— 这里起**生产的**代理(默认接线 = 注册表)并按生产 +// sidecarEgressProxyEnv 给引擎 env,与出货形态同一条路;判据仍只看文件轴(网络轴在 network-egress-fence.test.ts)。 +import { startEgressPolicyProxy, type EgressProxyHandle } from "./network-egress-proxy" +import { sidecarEgressProxyEnv } from "./sidecar-env" const describeDarwin = process.platform === "darwin" && existsSync("/usr/bin/sandbox-exec") ? describe : describe.skip const repoRoot = resolve(import.meta.dir, "../../../..") @@ -49,10 +53,12 @@ describeDarwin("REQ-159 真引擎 + 真 ext 在进程围栏下:四类真消费 let addon = "" let profileFile = "" let planLog: string[] = [] + let egressProxy: EgressProxyHandle | undefined const engines: Engine[] = [] const landed = (dir: string) => readdirSync(dir).sort() - beforeAll(() => { + beforeAll(async () => { + egressProxy = await startEgressPolicyProxy({ log: () => {} }) // ① ext bundle:本树的 ext(不是别的 worktree 的),现编。 const ext = spawnSync(process.execPath, ["run", "build"], { cwd: join(repoRoot, "packages", "ext"), encoding: "utf8", timeout: 120_000 }) if (ext.status !== 0 || !existsSync(extBundle)) throw new Error(`ext build failed (本次测量作废): ${ext.stderr}`) @@ -72,6 +78,7 @@ describeDarwin("REQ-159 真引擎 + 真 ext 在进程围栏下:四类真消费 userDataPath: userData, sidecarEnv: { HOME: home, XDG_DATA_HOME: join(home, ".local", "share"), XDG_CACHE_HOME: join(home, ".cache"), XDG_CONFIG_HOME: join(home, ".config"), XDG_STATE_HOME: userData }, addon: { packaged: true, resourcesPath: join(iso, "resources"), moduleDir: "/unused", exists: existsSync }, + egressProxyPort: egressProxy.port, }, { homeDir: () => home, @@ -97,6 +104,7 @@ describeDarwin("REQ-159 真引擎 + 真 ext 在进程围栏下:四类真消费 }) afterAll(async () => { + await egressProxy?.close() for (const e of engines) { try { e.proc.kill("SIGTERM") @@ -125,8 +133,8 @@ describeDarwin("REQ-159 真引擎 + 真 ext 在进程围栏下:四类真消费 XDG_CONFIG_HOME: join(home, ".config"), XDG_STATE_HOME: userData, TMPDIR: process.env.TMPDIR ?? "/tmp", - NO_PROXY: "127.0.0.1,localhost", - no_proxy: "127.0.0.1,localhost", + // `#1337`:与出货 sidecar 同一份代理栈(HTTP(S)_PROXY → 策略代理;NO_PROXY = loopback)。 + ...sidecarEgressProxyEnv(egressProxy!.port), NO_COLOR: "1", OPENCODE_SERVER_USERNAME: "opencode", OPENCODE_SERVER_PASSWORD: PASSWORD, diff --git a/packages/ui-mac/src/main/process-fence-plan.test.ts b/packages/ui-mac/src/main/process-fence-plan.test.ts index 42a36742fbd4..6c11054fdc77 100644 --- a/packages/ui-mac/src/main/process-fence-plan.test.ts +++ b/packages/ui-mac/src/main/process-fence-plan.test.ts @@ -39,6 +39,7 @@ function harness(overrides: Partial = {}, existing = new S userDataPath: USER_DATA, sidecarEnv: { HOME }, addon: { packaged: true, resourcesPath: "/App/Contents/Resources", moduleDir: "/unused", exists: (p) => existing.has(p) }, + egressProxyPort: 4443, } return { deps, input, made, logs } } @@ -53,7 +54,10 @@ describeDarwin("planProcessFence", () => { expect(plan.attempts).toBe(1) expect(plan.profileBytes).toBe(Buffer.byteLength(plan.profile)) expect(h.made).toEqual([`${HOME}/.local/share/opencode`, `${HOME}/.cache/opencode`, `${HOME}/.config/opencode`, `${HOME}/.npm`]) - expect(h.logs.some((l) => /process fence planned: workspaces=3 \(candidates=3, excluded=0, dropped=0\)/.test(l))).toBe(true) + expect(h.logs.some((l) => /process fence planned: workspaces=3 \(candidates=3, excluded=0, dropped=0\), egressProxyPort=4443/.test(l))).toBe(true) + // `#1337`:计划里的 profile 带着那扇门(N4 = 传入的代理端口),不是别的端口 + expect(plan.profile).toContain('(allow network-outbound (remote ip "localhost:4443"))') + expect(plan.profile).toContain("(deny network*)") // W16 / W10 / W17 / W18 刻意不建 expect(h.made.some((d) => d.endsWith("/.opencode") || d.endsWith("/.zsh_sessions") || d.includes("bun"))).toBe(false) }) @@ -90,9 +94,22 @@ describeDarwin("planProcessFence", () => { expect(h.logs.some((l) => l.includes("dropped=1") && l.includes(`${HOME}/proj-a`) && l.includes("exceeds maximum"))).toBe(true) }) - test("fail-closed ①:最小集也编不过 ⇒ 抛,原因原文可读", () => { - const h = harness({ compile: () => ({ ok: false, reason: "profile compilation failed" }) }) - expect(() => planProcessFence(h.input, h.deps)).toThrow(/minimum writable set.*profile compilation failed/) + test("fail-closed ①:字节墙之外的编译失败 ⇒ 一个工作区都不丢就抛,原因原文可读,且点名「不是并集大小」(`#1337` 归因)", () => { + const h = harness({ compile: () => ({ ok: false, reason: "sandbox-exec: unbound variable: host … line 25, column 33" }) }) + expect(() => planProcessFence(h.input, h.deps)).toThrow(/not the 65535-byte data-object wall.*none were dropped \(3 workspaces, 0 dropped, 1 attempt\): sandbox-exec: unbound variable: host/) + }) + + test("fail-closed ①′:字节墙丢到只剩默认工作区仍编不过 ⇒ 抛「最小可写集」,账目可读", () => { + const h = harness({ compile: () => ({ ok: false, reason: "sandbox-exec: data object length 70173 exceeds maximum (65535)" }) }) + expect(() => planProcessFence(h.input, h.deps)).toThrow(/minimum writable set \(1 workspace, 2 dropped, 3 attempts\): .*exceeds maximum/) + }) + + test("fail-closed ④(`#1337`):代理端口不合法 ⇒ 不产计划(围栏没有那扇门就不许装)", () => { + const h = harness() + for (const bad of [0, 65536, 1.5, Number.NaN]) { + h.input.egressProxyPort = bad + expect(() => planProcessFence(h.input, h.deps), String(bad)).toThrow(/egress proxy port must be an integer in 1\.\.65535/) + } }) test("fail-closed ②:原生模块不在 ⇒ 抛并点名路径(打包漏了 extraResources 就是这一格)", () => { diff --git a/packages/ui-mac/src/main/process-fence-plan.ts b/packages/ui-mac/src/main/process-fence-plan.ts index 7df0ca431def..c6847af35713 100644 --- a/packages/ui-mac/src/main/process-fence-plan.ts +++ b/packages/ui-mac/src/main/process-fence-plan.ts @@ -46,6 +46,11 @@ export type PlanProcessFenceInput = { /** createSidecarEnv() 的输出 —— XDG_* / HOME 从**这一份**取,它才是引擎将看到的 env。 */ sidecarEnv: Record addon: FenceAddonResolveInput + /** + * REQ-137 `#1337`:main 进程内策略代理**已经在听**的 loopback 端口(server.ts 在调用本函数之前起好)。 + * 渲染进 profile 的 N4 行;没有它就没有计划(fail-closed)。 + */ + egressProxyPort: number } export type PlanProcessFenceDeps = { @@ -98,7 +103,7 @@ export function planProcessFence(input: PlanProcessFenceInput, deps: PlanProcess throw new Error("process fence: the default workspace is not a directory on disk — refusing to fork an engine that could not write anywhere a user expects") const trimmed = trimUntilCompiles( - { workspaces: union.selected, alphaGlobalRoot, userDataPath: input.userDataPath, roots, stateHome }, + { workspaces: union.selected, alphaGlobalRoot, userDataPath: input.userDataPath, roots, stateHome, egressProxyPort: input.egressProxyPort }, deps.compile, ) const addonPath = resolveFenceAddonPath(input.addon) @@ -113,7 +118,7 @@ export function planProcessFence(input: PlanProcessFenceInput, deps: PlanProcess roots: { ...roots, alphaGlobalRoot, userDataPath: input.userDataPath, stateHome }, } deps.log( - `process fence planned: workspaces=${plan.workspaces.length} (candidates=${union.candidates}, excluded=${plan.excluded.length}, dropped=${plan.dropped.length}), profile=${plan.profileBytes}B, compile attempts=${plan.attempts}` + + `process fence planned: workspaces=${plan.workspaces.length} (candidates=${union.candidates}, excluded=${plan.excluded.length}, dropped=${plan.dropped.length}), egressProxyPort=${input.egressProxyPort}, profile=${plan.profileBytes}B, compile attempts=${plan.attempts}` + (plan.dropped.length ? ` — dropped (oldest first): ${[...plan.dropped].reverse().join(", ")}; last compiler error: ${trimmed.lastFailure}` : "") + (plan.excluded.length ? ` — excluded: ${plan.excluded.map((e) => `${e.directory} (${e.reason})`).join("; ")}` : ""), ) diff --git a/packages/ui-mac/src/main/process-fence-profile.test.ts b/packages/ui-mac/src/main/process-fence-profile.test.ts index 0bd4a7c5143e..4485e8c19d05 100644 --- a/packages/ui-mac/src/main/process-fence-profile.test.ts +++ b/packages/ui-mac/src/main/process-fence-profile.test.ts @@ -10,8 +10,10 @@ import { spawnSync } from "node:child_process" import { createRequire } from "node:module" import { join, resolve } from "node:path" import { + COMPILE_BYTE_WALL, MAX_WORKSPACES, WRITABLE_ROOT_IDS, + assertEgressProxyPort, assertSeatbeltSafePath, renderProcessFenceProfile, resolveEngineRoots, @@ -29,8 +31,17 @@ const base: ProcessFenceProfileInput = { userDataPath: `${HOME}/Library/Application Support/ai.opencode.desktop`, stateHome: `${HOME}/Library/Application Support/ai.opencode.desktop`, roots, + egressProxyPort: 4443, } +/** `#1334` Q1.2 的四行(逐字,独立字面量 —— 不从渲染器取);端口是唯一的参数。 */ +const NETWORK_LINES = (port: number) => [ + "(deny network*)", + '(allow network-bind (local ip "localhost:*"))', + '(allow network-inbound (local ip "localhost:*"))', + `(allow network-outbound (remote ip "localhost:${port}"))`, +] + /** 去掉每行的 `; Wn` 注释与多余空白,只比策略 token —— 注释在编译期被剥离(勘破 §6.5 第 9 行)。 */ const tokens = (profile: string) => profile @@ -65,9 +76,28 @@ describe("renderProcessFenceProfile —— 勘破 §8.2 的 19 行,逐行全等" `(subpath "${HOME}/Library/Caches/bun")`, `(subpath "${HOME}/.cache/bun")`, ")", + ...NETWORK_LINES(4443), ]) }) + test("`#1337` 网络行:Q1.2 四行逐字、顺序不变、deny 在前 allow 在后、N4 的端口就是传入的代理端口;只写加法(没有第二条 deny)", () => { + const out = tokens(renderProcessFenceProfile({ ...base, egressProxyPort: 61234 })) + expect(out.slice(-4)).toEqual(NETWORK_LINES(61234)) + expect(out.filter((l) => l.startsWith("(deny "))).toEqual(["(deny file-write*)", "(deny network*)"]) + expect(out.filter((l) => /network-outbound/.test(l))).toEqual([`(allow network-outbound (remote ip "localhost:61234"))`]) + // DNS 刻意不放行:没有 mDNSResponder 那一行(`#1334` Q3:解析搬到代理那一侧) + expect(renderProcessFenceProfile(base)).not.toContain("mDNSResponder") + }) + + test("`#1337` fail-closed:代理端口不是 1..65535 的整数 ⇒ 拒绝渲染(没有那扇门就没有 profile)", () => { + for (const bad of [0, 65536, -1, 1.5, Number.NaN, "4443", undefined, null]) { + expect(() => assertEgressProxyPort(bad), String(bad)).toThrow(/egress proxy port must be an integer in 1\.\.65535/) + expect(() => renderProcessFenceProfile({ ...base, egressProxyPort: bad as number }), String(bad)).toThrow(/egress proxy port/) + } + expect(assertEgressProxyPort(1)).toBe(1) + expect(assertEgressProxyPort(65535)).toBe(65535) + }) + test("每一行都带 §8.2 的 id 注释,且 id 集合 = WRITABLE_ROOT_IDS 的键(登记簿只能点名存在的行)", () => { const ids = new Set( renderProcessFenceProfile(base) @@ -252,11 +282,27 @@ describe("trimUntilCompiles —— 试编译封顶,从尾部丢,丢到底仍失 expect(tokens(r.profile)).not.toContain(`(subpath "/Users/alpha/b")`) }) - test("默认工作区不可丢:只剩 1 个仍失败 ⇒ 抛,消息带编译器原文与账目(fail-closed 到「引擎不起」)", () => { - expect(() => trimUntilCompiles({ ...base, workspaces: ws }, compileAllowing(0, "profile compilation failed"))).toThrow( - /minimum writable set \(1 workspace, 3 dropped, 4 attempts\): profile compilation failed/, + test("默认工作区不可丢:字节墙丢到只剩 1 个仍失败 ⇒ 抛,消息带编译器原文与账目(fail-closed 到「引擎不起」)", () => { + expect(() => trimUntilCompiles({ ...base, workspaces: ws }, compileAllowing(0))).toThrow( + /minimum writable set \(1 workspace, 3 dropped, 4 attempts\): sandbox-exec: data object length 70173 exceeds maximum \(65535\)/, ) }) + + test("`#1337` 归因:失败原因不是字节墙(语法坏 / unbound variable)⇒ 一个工作区都不丢、attempts=1 即抛,消息点名「丢工作区救不了」", () => { + const seen: number[] = [] + const syntaxBad = (profile: string) => { + seen.push(profile.match(/; W1$/gm)?.length ?? 0) + return { ok: false as const, reason: "sandbox-exec: unbound variable: host … line 25, column 33" } + } + expect(() => trimUntilCompiles({ ...base, workspaces: ws }, syntaxBad)).toThrow( + /not the 65535-byte data-object wall — dropping workspaces cannot fix it, so none were dropped \(4 workspaces, 0 dropped, 1 attempt\): sandbox-exec: unbound variable: host/, + ) + expect(seen).toEqual([4]) // 只试编了一次,而且是全并集 + // 字节墙的原文由 process-fence-compile.test.ts 对真编译器钉住;这里只核对判别式认得它、不认别的 + expect(COMPILE_BYTE_WALL.test("sandbox-exec: data object length 65574 exceeds maximum (65535)")).toBe(true) + expect(COMPILE_BYTE_WALL.test("sandbox-exec: unbound variable: host")).toBe(false) + expect(COMPILE_BYTE_WALL.test("profile compilation failed")).toBe(false) + }) }) describe("控制组:判据能测出已知的坏", () => { @@ -264,6 +310,13 @@ describe("控制组:判据能测出已知的坏", () => { const rendered = tokens(renderProcessFenceProfile(base)).filter((l) => !l.includes("/dev/ptmx")) expect(rendered).not.toEqual(tokens(renderProcessFenceProfile(base))) }) + test("`#1337` 少 N3(inbound)或多一条减法 deny ⇒ 网络行判据红(老勘破 §5 那两行 / 减法组合正是 `#1334` 实测的两种坏)", () => { + const good = tokens(renderProcessFenceProfile(base)) + const sec5 = good.filter((l) => !l.includes("network-inbound")) + expect(sec5.slice(-4)).not.toEqual(NETWORK_LINES(4443)) + const subtractive = [...good, '(deny network-outbound (remote ip "localhost:1234"))'] + expect(subtractive.filter((l) => l.startsWith("(deny "))).not.toEqual(["(deny file-write*)", "(deny network*)"]) + }) test("放宽一行(HOME 整个进 subpath)⇒ 并集规则红", () => { const union = selectWorkspaceUnion({ sources: { tabs: [{ type: "draft", draftID: "d", server: "sidecar", directory: "/Users/alpha" }], recent: undefined, info: undefined }, diff --git a/packages/ui-mac/src/main/process-fence-profile.ts b/packages/ui-mac/src/main/process-fence-profile.ts index 611c6a87bfe9..56cb050065de 100644 --- a/packages/ui-mac/src/main/process-fence-profile.ts +++ b/packages/ui-mac/src/main/process-fence-profile.ts @@ -22,6 +22,15 @@ // 加载原生模块与 apply 在 process-fence-apply.ts。分层是为了让每一层各自可测:这里的判据是 // 「渲染出的 profile 逐 token 等于 §8.2 的形状」(process-fence-profile.test.ts)。 // +// ── 网络行(REQ-137 `#1337`):`#1334` Q1.2 那四行,逐字 ────────────────────────────── +// `(deny network*)` + loopback 的 `network-bind` / `network-inbound` + 只放行 loopback 上策略代理端口的 +// `network-outbound`。四条硬要求全部来自 `#1334` 实测:①必须放行本地 bind / inbound —— 老勘破 §5 那两行照抄 +// ⇒ sidecar(它就是 HTTP 服务器)`listen()` EPERM,出货形态 560 ms 死亡且五份日志零原因(Q1.1);②只写加法 —— +// 减法 `deny` 在 `deny network*` + `localhost:*` 组合下静默失效(老勘破 §1.3);③`(deny network*)` 连带拦掉 DNS +// (mDNSResponder 的 unix socket,Q2)—— 刻意不放行:走代理的客户端由代理解析,想直连的死在解析这一步(Q3); +// ④字节余量 39 倍(Q5),不为它做任何优化。代理跑在 Electron main(围栏外,`#1073` 裁决三),端口由 server.ts +// 在 fork 之前给出;策略(目的地授权)在 network-egress-proxy.ts / network-egress-registry.ts,不在这里。 +// // ── 不要手写 `(subpath …)` 的 TypeScript 替身 ──────────────────────────────────────── // U2 裁决 §5.2 实测 seatbelt 的 subpath 按路径分段、解析软链、随卷的大小写策略匹配,字符串 // 谓词在三处说谎。本文件里凡是「这个目录在不在可写集里」的问题都不回答;唯一例外是并集的 @@ -94,6 +103,18 @@ export type ProcessFenceProfileInput = { */ stateHome: string roots: EngineRoots + /** + * REQ-137 `#1337`:loopback 上策略代理的端口 —— sidecar 那棵树 TCP/UDP 出网的**唯一**放行。main 在 fork 之前 + * 起代理再渲染(server.ts),所以这里永远是一个已经在听的真端口;没有端口就没有 profile(fail-closed)。 + */ + egressProxyPort: number +} + +/** 端口必须是 1..65535 的整数 —— 渲染进 `(remote ip "localhost:")` 的是字面量,不是表达式;坏值 = 坏 profile。 */ +export function assertEgressProxyPort(port: unknown): number { + if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) + throw new Error(`process fence: egress proxy port must be an integer in 1..65535, got ${JSON.stringify(port)}`) + return port } /** seatbelt 字符串字面量里我们**不解释**转义:含引号 / 反斜杠 / 控制字符的路径直接拒绝(fail-closed)。 */ @@ -122,6 +143,7 @@ export function renderProcessFenceProfile(input: ProcessFenceProfileInput): stri const cacheHome = assertSeatbeltSafePath(input.roots.cacheHome, "XDG_CACHE_HOME") const configHome = assertSeatbeltSafePath(input.roots.configHome, "XDG_CONFIG_HOME") const stateHome = assertSeatbeltSafePath(input.stateHome, "XDG_STATE_HOME") + const egressProxyPort = assertEgressProxyPort(input.egressProxyPort) const homeRe = regexLiteral(home) const lines: string[] = [ @@ -149,6 +171,11 @@ export function renderProcessFenceProfile(input: ProcessFenceProfileInput): stri ` (subpath "${join(home, "Library", "Caches", "bun")}") ; W17`, ` (subpath "${join(home, ".cache", "bun")}") ; W18`, ")", + // ── 网络行:`#1334` Q1.2 逐字(顺序、形状都不改;`network-bind` 按 Q1.1 是冗余的,但它是 bind() 语义的显式声明) + "(deny network*) ; N1 出网默认全拒(含 DNS 的 unix socket,Q2)", + '(allow network-bind (local ip "localhost:*")) ; N2 sidecar 自己的 HTTP 服务器要 bind', + '(allow network-inbound (local ip "localhost:*")) ; N3 …还要 accept —— 缺它 listen() EPERM(Q1.1)', + `(allow network-outbound (remote ip "localhost:${egressProxyPort}")) ; N4 唯一出口:loopback 上的策略代理(main 进程内)`, ] return lines.join("\n") + "\n" } @@ -311,8 +338,15 @@ export function selectWorkspaceUnion(input: WorkspaceUnionInput): WorkspaceUnion // seatbelt 编译有一道硬墙:`data object length … exceeds maximum (65535)`,单位是编译后数据对象的 // 字节,**没有精确刻画**(220 字符 × 340 条 = 74 513 B 仍通过而工具报 76 413;U2 §6)。所以不许靠算: // 用**真编译器**试一次,失败就从并集尾部丢一个工作区再试,直到通过;丢到只剩 `~/code-puppy`(并集首位, -// 不可丢)仍失败 ⇒ 抛出 —— 这是 fail-closed 到「引擎不起」的那一档,而不是「少放几个工作区」: -// 到这一步说明失败原因不是并集大小,是别的(profile 语法 / 根路径异常),放行会是「前提为假的闸门」。 +// 不可丢)仍失败 ⇒ 抛出 —— 这是 fail-closed 到「引擎不起」的那一档,而不是「少放几个工作区」。 +// +// ── 归因(REQ-137 `#1337`,`#1334` Q5 末节)──────────────────────────────────────────── +// 裁剪只丢工作区,网络行不可丢。所以**只有**编译器报的是那道字节墙(`exceeds maximum`)时,丢工作区才可能 +// 有用;别的失败(语法坏了、根路径异常 —— 比如把老勘破 §1.1 已证不存在的 `(remote host …)` 写进网络行) +// 丢光工作区也编不过,却会抛出「最小可写集都编不过(4 dropped, 5 attempts)」,把运维指向并集大小。 +// 因此第一次失败若不是字节墙 ⇒ **一个工作区都不丢**,当场抛,消息点名「这不是并集大小的问题」。 +// `exceeds maximum` 这个原文由 process-fence-compile.test.ts 对着真 /usr/bin/sandbox-exec 钉住(已知撞墙的 +// profile 必须报它),不是这里猜的。 export type TrialCompile = (profile: string) => { ok: true } | { ok: false; reason: string } @@ -327,6 +361,9 @@ export type TrimResult = { lastFailure?: string } +/** libsandbox 撞 65535 字节墙时的原文片段(process-fence-compile.test.ts 对真编译器钉住)。 */ +export const COMPILE_BYTE_WALL = /exceeds maximum/ + export function trimUntilCompiles( input: Omit & { workspaces: readonly string[] }, compile: TrialCompile, @@ -341,6 +378,12 @@ export function trimUntilCompiles( const result = compile(profile) if (result.ok) return { profile, workspaces, dropped, attempts, lastFailure } lastFailure = result.reason + if (!COMPILE_BYTE_WALL.test(result.reason)) { + // 不是字节墙 ⇒ 丢工作区救不了,别把运维指向并集大小。 + throw new Error( + `process fence profile does not compile, and the compiler error is not the 65535-byte data-object wall — dropping workspaces cannot fix it, so none were dropped (${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"}, ${dropped.length} dropped, ${attempts} attempt${attempts === 1 ? "" : "s"}): ${result.reason}`, + ) + } if (workspaces.length <= 1) { throw new Error( `process fence profile does not compile even with the minimum writable set (${workspaces.length} workspace, ${dropped.length} dropped, ${attempts} attempts): ${result.reason}`, diff --git a/packages/ui-mac/src/main/process-fence-wiring.test.ts b/packages/ui-mac/src/main/process-fence-wiring.test.ts index 1315941072b3..4a92a59082b0 100644 --- a/packages/ui-mac/src/main/process-fence-wiring.test.ts +++ b/packages/ui-mac/src/main/process-fence-wiring.test.ts @@ -8,10 +8,15 @@ // sidecar.ts 顶层的 registerHooks / getParentPort() 让它结构上无法被 import,所以「sidecar 收到命令后 // 第一件事就是 apply、缺席即拒」这一跳只能锚源码(文末 ANCHOR,不是闸门;行为判据是 apply 测试里的 // C1–C5 —— 那五种失败都是 installProcessFence 抛出去、经 start() 的 catch 变成 error IPC + exit(1))。 +// +// `#1337`(REQ-137)在同一条线上多守三跳(darwin):策略代理先于计划起来,端口进计划器 + 整份改写进 fork 的 env +// (用户自己的 HTTP(S)_PROXY / NO_PROXY 不能存活 —— 唯一通路);代理起不来 ⇒ 拒 fork、零 fork;缺省提供者真起监听。 +// 非 darwin:没有围栏就不装策略层 —— 不起代理、env 照旧。 import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" import { EventEmitter } from "node:events" import { mkdtempSync, readFileSync, rmSync } from "node:fs" +import * as net from "node:net" import { tmpdir } from "node:os" import { join } from "node:path" @@ -42,6 +47,11 @@ mock.module("./store", () => ({ getStore: () => ({ get: () => null, set: () => { // 陷阱:`await import("./server")` 必须排在 mock.module("electron", ...) **之后**,否则真 electron 会被拉起来。 const { spawnLocalServer } = await import("./server") const { creditDanglingSweepForSpawn, resetDanglingSweepLatchForTests } = await import("./dangling-sweep-latch") +const { SIDECAR_EGRESS_NO_PROXY } = await import("./sidecar-env") + +const darwin = process.platform === "darwin" +/** 假代理提供者:不起监听,只给端口(默认接线的真监听在末尾那条用例里验)。 */ +const fakeEgress = (port = 4433) => async () => ({ port }) class RecordingChild extends EventEmitter { stdout = new EventEmitter() @@ -67,7 +77,7 @@ class RecordingChild extends EventEmitter { let userDataPath = "" const savedEnv: Record = {} -const managedEnv = ["SHELL", "ALPHA_SECRETS_DISABLE"] as const +const managedEnv = ["SHELL", "ALPHA_SECRETS_DISABLE", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "https_proxy", "http_proxy", "no_proxy", "ALL_PROXY", "all_proxy"] as const beforeEach(() => { userDataPath = mkdtempSync(join(tmpdir(), "fence-wiring-")) @@ -102,6 +112,7 @@ describe("REQ-159 main 侧接线:计划 → start 命令 → 拒 fork", () => { forks++ return child }) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: fakeEgress(), planFence: (input) => { // 计划拿到的是 sidecar 将拿到的 env(白名单之后),不是 main 的 process.env expect(input.userDataPath).toBe(userDataPath) @@ -125,6 +136,7 @@ describe("REQ-159 main 侧接线:计划 → start 命令 → 拒 fork", () => { userDataPath, healthCheck: async () => true, fork: (() => child) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: fakeEgress(), planFence: () => ({ profile: "(version 1)\n(allow default)\n(deny file-write*)\n", addonPath: "/x/alpha_fence.node" }), }) await result.health.wait @@ -150,6 +162,7 @@ describe("REQ-159 main 侧接线:计划 → start 命令 → 拒 fork", () => { forks++ return new RecordingChild() }) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: fakeEgress(), planFence: () => { throw new Error("process fence profile does not compile even with the minimum writable set (1 workspace, 0 dropped, 1 attempts): profile compilation failed") }, @@ -169,6 +182,7 @@ describe("REQ-159 main 侧接线:计划 → start 命令 → 拒 fork", () => { forks++ return child }) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: fakeEgress(), planFence: () => undefined, }) if (process.platform === "darwin") { @@ -184,6 +198,130 @@ describe("REQ-159 main 侧接线:计划 → start 命令 → 拒 fork", () => { } }) + test("`#1337` darwin:代理端口进计划器,且 fork 的 env 被整份改写成指向它 —— 用户自己的 HTTPS_PROXY / NO_PROXY 不存活;非 darwin 不起代理、env 照旧", async () => { + process.env.HTTPS_PROXY = "http://127.0.0.1:7897" + process.env.https_proxy = "http://127.0.0.1:7897" + process.env.NO_PROXY = "github.com,.internal" + const child = new RecordingChild() + let forkEnv: Record = {} + let planned: number | undefined = -1 + let egressCalls = 0 + creditDanglingSweepForSpawn() + const result = await spawnLocalServer("127.0.0.1", 4315, "password", { + userDataPath, + healthCheck: async () => true, + fork: ((_: string, __: string[], opts: { env: Record }) => { + forkEnv = { ...opts.env } + return child + }) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: async () => { + egressCalls++ + return { port: 51337 } + }, + planFence: (input) => { + planned = input.egressProxyPort + return darwin ? { profile: "(version 1)\n(allow default)\n(deny file-write*)\n", addonPath: "/x/alpha_fence.node" } : undefined + }, + }) + await result.health.wait + if (darwin) { + expect(egressCalls).toBe(1) + expect(planned).toBe(51337) + for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"]) expect(forkEnv[key], key).toBe("http://127.0.0.1:51337") + expect(forkEnv.NO_PROXY).toBe(SIDECAR_EGRESS_NO_PROXY) + expect(forkEnv.no_proxy).toBe(SIDECAR_EGRESS_NO_PROXY) + expect(JSON.stringify(forkEnv)).not.toContain("7897") + expect(JSON.stringify(forkEnv)).not.toContain("github.com") + // main 自己的 env 一个字都没动(main / renderer 的出网不在覆盖内) + expect(process.env.HTTPS_PROXY).toBe("http://127.0.0.1:7897") + expect(process.env.NO_PROXY).toBe("github.com,.internal") + } else { + expect(egressCalls).toBe(0) + expect(planned).toBeUndefined() + expect(forkEnv.HTTPS_PROXY).toBe("http://127.0.0.1:7897") + expect(forkEnv.NO_PROXY).toBe("github.com,.internal") + } + await result.listener.stop() + }) + + test("`#1337` darwin:代理起不来 ⇒ spawn 拒绝、消息带原因、fork 一次都不发生(fail-closed);非 darwin 不受影响", async () => { + let forks = 0 + let planCalls = 0 + creditDanglingSweepForSpawn() + const attempt = spawnLocalServer("127.0.0.1", 4316, "password", { + userDataPath, + healthCheck: async () => true, + fork: (() => { + forks++ + return new RecordingChild() + }) as unknown as typeof import("electron").utilityProcess.fork, + egressProxy: async () => { + throw new Error("listen EADDRINUSE: address already in use 127.0.0.1:0") + }, + planFence: () => { + planCalls++ + return darwin ? { profile: "(version 1)\n(allow default)\n(deny file-write*)\n", addonPath: "/x/alpha_fence.node" } : undefined + }, + }) + if (darwin) { + await expect(attempt).rejects.toThrow(/network egress policy proxy failed to start — sidecar fork refused: .*EADDRINUSE/) + expect(forks).toBe(0) + expect(planCalls).toBe(0) + } else { + const result = await attempt + await result.health.wait + expect(forks).toBe(1) + await result.listener.stop() + } + }) + + test("`#1337` darwin:缺省提供者真起一个策略代理 —— fork env 里那个端口在听,且它只做 CONNECT(明文 GET 405);两次 fork 同一端口(跨代复用)", async () => { + if (!darwin) return + const ports: number[] = [] + for (const port of [4317, 4318]) { + const child = new RecordingChild() + let forkEnv: Record = {} + creditDanglingSweepForSpawn() + const result = await spawnLocalServer("127.0.0.1", port, "password", { + userDataPath, + healthCheck: async () => true, + fork: ((_: string, __: string[], opts: { env: Record }) => { + forkEnv = { ...opts.env } + return child + }) as unknown as typeof import("electron").utilityProcess.fork, + planFence: (input) => { + expect(input.egressProxyPort).toBe(Number(new URL(forkEnv.HTTPS_PROXY ?? "http://127.0.0.1:0").port) || input.egressProxyPort) + return { profile: "(version 1)\n(allow default)\n(deny file-write*)\n", addonPath: "/x/alpha_fence.node" } + }, + }) + await result.health.wait + ports.push(Number(new URL(forkEnv.HTTPS_PROXY).port)) + await result.listener.stop() + } + expect(ports[0]).toBeGreaterThan(0) + expect(ports[1]).toBe(ports[0]) + // 读满 Content-Length 即返(bun 的 http 连接 socket 上等对端 close 不可靠,network-egress-proxy.test.ts 同一口径) + const reply = await new Promise((resolve, reject) => { + let text = "" + const s = net.connect(ports[0], "127.0.0.1", () => s.write("GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n")) + const finish = () => { + resolve(text) + s.destroy() + } + s.on("data", (c) => { + text += c.toString() + const sep = text.indexOf("\r\n\r\n") + if (sep < 0) return + const length = /content-length:\s*(\d+)/i.exec(text.slice(0, sep)) + if (length && Buffer.byteLength(text.slice(sep + 4)) >= Number(length[1])) finish() + }) + s.on("close", finish) + s.on("error", reject) + }) + expect(reply).toMatch(/^HTTP\/1\.1 405 /) + expect(reply).toContain("alpha egress policy") + }) + test("ANCHOR (not a gate): sidecar.ts 收到 start 后第一件事是 installProcessFence,早于注入与 import 引擎;darwin 缺席即抛", () => { const source = readFileSync(join(import.meta.dir, "sidecar.ts"), "utf8") const fence = source.indexOf("installProcessFence(command)") diff --git a/packages/ui-mac/src/main/server.ts b/packages/ui-mac/src/main/server.ts index b839837b82c4..cd0a9d955344 100644 --- a/packages/ui-mac/src/main/server.ts +++ b/packages/ui-mac/src/main/server.ts @@ -12,7 +12,7 @@ import { loadAlphaSecrets } from "./alpha-secrets" import { posixModesEffective } from "./platform" import { pollUntilHealthy } from "./health-poll" import { getLogger, rotateServerLogs, write } from "./logging" -import { createSidecarEnv } from "./sidecar-env" +import { createSidecarEnv, sidecarEgressProxyEnv } from "./sidecar-env" import { getUserShell, loadShellEnv } from "./shell-env" import { probeShellEnvAsync, readShellEnvCache, sanitizeCachedShellEnv, writeShellEnvCache } from "./shell-env-cache" import { getStore } from "./store" @@ -28,6 +28,12 @@ import { consumeDanglingSweepCredit } from "./dangling-sweep-latch" import { planProcessFence, type ProcessFencePlan } from "./process-fence-plan" import { trialCompileProfile } from "./process-fence-compile" import type { ProcessFenceStartInput } from "./process-fence-apply" +// REQ-137 `#1337`:网络出网的强制半场接线。围栏(profile N1–N4)把 sidecar 那棵树的出网收成 loopback 上**一个**端口, +// 那个端口上听着的是本进程(main,围栏外 —— `(deny network*)` 连带拦 DNS,代理必须在围栏外才解析得了域名, +// `#1073` 裁决三)里的策略代理;它只放行 network-egress-registry.ts 登记过的 `host:port`。代理在第一次 fork 之前起、 +// 跨 respawn 复用;起不来 = 拒绝 fork(与围栏计划失败同一条 fail-closed 路)。sidecar 的八个代理变量在这里整份改写 +// 成指向它(sidecarEgressProxyEnv),main 自己的 process.env 一个字都不动 —— main / renderer 的出网不在覆盖内(AC5)。 +import { startEgressPolicyProxy, type EgressLogRecord, type EgressProxyHandle } from "./network-egress-proxy" // REQ-159 `#1322`:工作区写探针的 main 半场 —— 请求/应答簿记住在 workspace-write-probe.ts,这里只接线到子进程。 import { createWriteProbeRequester, type WorkspaceWriteProbeResult } from "./workspace-write-probe" import { alphaGlobalRoot } from "./engine-config-truth" @@ -91,18 +97,52 @@ type SpawnLocalServerOptions = { * 测试用假子进程时注入替身。返回 undefined 只允许在非 darwin(那里没有 seatbelt);darwin 上 * 生产计划器要么给出计划、要么抛出 —— 抛出 = 这一代 fork 被拒(与 alpha-secrets sync 失败同一条路)。 */ - planFence?: (input: { userDataPath: string; sidecarEnv: Record }) => ProcessFenceStartInput | undefined + planFence?: (input: { userDataPath: string; sidecarEnv: Record; egressProxyPort: number | undefined }) => ProcessFenceStartInput | undefined + /** + * REQ-137 `#1337`:策略代理的提供者。缺省 = 生产单例(ensureEgressPolicyProxy:本进程内真起一个,跨 fork 复用); + * 测试注入一个假端口以免每条用例都起监听。reject = 这一代 fork 被拒。只在 darwin 上被调用 —— 围栏只在那里, + * 没有强制层的平台不装策略层(如实:那里的引擎本来就没有围栏,不假装有一半)。 + */ + egressProxy?: () => Promise<{ port: number }> +} + +// ── REQ-137:策略代理单例(main 进程内,围栏外)────────────────────────────────────────── +// 一次起、跨 respawn 复用:端口写进每一代的 profile(N4)与 sidecar env,换代不换端口。起失败时把单例清掉, +// 下一次 fork 再试(不把一次失败钉成永久失败)。它的结构化记录(每条 CONNECT 的 allow / deny 与原因)进 main 日志, +// deny 走 warn —— 「工具把数据发到未授权目的地时失败且可见」的可见就在这一行。 +let egressProxySingleton: Promise | undefined +function ensureEgressPolicyProxy(): Promise { + if (!egressProxySingleton) { + const log = (record: EgressLogRecord) => { + const line = `network egress ${JSON.stringify(record)}` + if (record.event === "egress.connect" && record.verdict === "deny") getLogger()?.warn(line) + else getLogger()?.log(line) + } + egressProxySingleton = startEgressPolicyProxy({ log }).then( + (handle) => { + getLogger()?.log(`network egress policy proxy listening on ${handle.host}:${handle.port} — the only way out of the engine tree (REQ-137); destinations = network-egress-registry.ts`) + return handle + }, + (error) => { + egressProxySingleton = undefined + throw error + }, + ) + } + return egressProxySingleton } /** 生产计划器:把 electron / store / fs 接进 electron-free 的 planProcessFence。 */ -function planProductionFence(input: { userDataPath: string; sidecarEnv: Record }): ProcessFencePlan | undefined { +function planProductionFence(input: { userDataPath: string; sidecarEnv: Record; egressProxyPort: number | undefined }): ProcessFencePlan | undefined { if (process.platform !== "darwin") return undefined + if (input.egressProxyPort === undefined) throw new Error("process fence plan: no egress proxy port on darwin — the fence's only outbound door is unknown, refusing to plan") const moduleDir = dirname(fileURLToPath(import.meta.url)) return planProcessFence( { userDataPath: input.userDataPath, sidecarEnv: input.sidecarEnv, addon: { packaged: app.isPackaged, resourcesPath: process.resourcesPath, moduleDir, exists: existsSync }, + egressProxyPort: input.egressProxyPort, }, { homeDir: homedir, @@ -353,9 +393,21 @@ export async function spawnLocalServer( // 与上面 alpha-secrets sync 失败同一条 fail-closed 路径)。env 先算好:计划要读的 XDG_* / HOME // 必须是 sidecar 将拿到的那一份,不是 main 自己的 process.env。 const sidecarEnv = createSidecarEnv() + // REQ-137:策略代理先于计划 —— 端口要渲染进 profile(N4)并改写进 sidecar env。起不来 = 拒 fork(fail-closed)。 + // 只在 darwin:围栏只在那里;别的平台既没有强制层也不装策略层(sidecar env 的代理变量照旧 = 用户导出的值)。 + let egressProxyPort: number | undefined + if (process.platform === "darwin") { + try { + egressProxyPort = (await (options.egressProxy ?? ensureEgressPolicyProxy)()).port + } catch (error) { + getLogger()?.error("network egress policy proxy FAILED to start — refusing to fork the sidecar (fail closed; a fenced engine with no door out must not start)", error) + throw new Error(`network egress policy proxy failed to start — sidecar fork refused: ${serializeError(error).message}`) + } + Object.assign(sidecarEnv, sidecarEgressProxyEnv(egressProxyPort)) + } let fence: ProcessFenceStartInput | undefined try { - fence = (options.planFence ?? planProductionFence)({ userDataPath: options.userDataPath, sidecarEnv }) + fence = (options.planFence ?? planProductionFence)({ userDataPath: options.userDataPath, sidecarEnv, egressProxyPort }) } catch (error) { getLogger()?.error("process fence plan FAILED — refusing to fork the sidecar (fail closed; an unfenced engine must not start)", error) throw new Error(`process fence plan failed — sidecar fork refused: ${serializeError(error).message}`) diff --git a/packages/ui-mac/src/main/sidecar-env.test.ts b/packages/ui-mac/src/main/sidecar-env.test.ts index 4cc8a158df11..f9dd4e1f86c1 100644 --- a/packages/ui-mac/src/main/sidecar-env.test.ts +++ b/packages/ui-mac/src/main/sidecar-env.test.ts @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test" import fs from "node:fs" import path from "node:path" -import { createSidecarEnv, isSecretish } from "./sidecar-env" +import { SIDECAR_EGRESS_NO_PROXY, createSidecarEnv, isSecretish, sidecarEgressProxyEnv } from "./sidecar-env" describe("createSidecarEnv — default-deny", () => { test("strips every secret named by the A6 acceptance criteria", () => { @@ -617,3 +617,32 @@ describe("createSidecarEnv — container-valued vars", () => { expect(env.ALPHA_GLOBAL_DIR).toBe("/Users/u/.alpha") }) }) + +// ── REQ-137 `#1337`:sidecar 的代理栈由策略代理独占决定 ───────────────────────────────── +describe("sidecarEgressProxyEnv (#1337)", () => { + test("八个代理变量整份给出:六个指向 loopback 策略代理、两个 NO_PROXY 只留 loopback;叠在 createSidecarEnv 之上时用户导出的值不存活", () => { + const env = sidecarEgressProxyEnv(51337) + expect(env).toEqual({ + HTTP_PROXY: "http://127.0.0.1:51337", + HTTPS_PROXY: "http://127.0.0.1:51337", + ALL_PROXY: "http://127.0.0.1:51337", + http_proxy: "http://127.0.0.1:51337", + https_proxy: "http://127.0.0.1:51337", + all_proxy: "http://127.0.0.1:51337", + NO_PROXY: SIDECAR_EGRESS_NO_PROXY, + no_proxy: SIDECAR_EGRESS_NO_PROXY, + }) + expect(SIDECAR_EGRESS_NO_PROXY).toBe("127.0.0.1,localhost,::1") + // 八个名字都在白名单里(否则 fork 时会被 createSidecarEnv 丢掉,而这里的覆盖就落不到 sidecar 上) + const user = { PATH: "/usr/bin", HOME: "/Users/u", HTTPS_PROXY: "http://127.0.0.1:7897", NO_PROXY: "github.com,.internal", ALL_PROXY: "socks5://127.0.0.1:1080" } + const merged = { ...createSidecarEnv(user), ...env } + for (const key of Object.keys(env)) expect(Object.keys(createSidecarEnv({ ...user, [key]: "x" })), key).toContain(key) + expect(merged.HTTPS_PROXY).toBe("http://127.0.0.1:51337") + expect(merged.ALL_PROXY).toBe("http://127.0.0.1:51337") + expect(merged.NO_PROXY).toBe(SIDECAR_EGRESS_NO_PROXY) + expect(JSON.stringify(merged)).not.toContain("7897") + expect(JSON.stringify(merged)).not.toContain("github.com") + // 坏端口 ⇒ 抛(fail-closed),不产一个指向 :0 / :NaN 的代理 URL + for (const bad of [0, 65536, 1.5, Number.NaN]) expect(() => sidecarEgressProxyEnv(bad), String(bad)).toThrow(/port must be an integer in 1\.\.65535/) + }) +}) diff --git a/packages/ui-mac/src/main/sidecar-env.ts b/packages/ui-mac/src/main/sidecar-env.ts index 36d8b3201265..f833c94a56a2 100644 --- a/packages/ui-mac/src/main/sidecar-env.ts +++ b/packages/ui-mac/src/main/sidecar-env.ts @@ -264,3 +264,33 @@ export function createSidecarEnv(source: NodeJS.ProcessEnv = process.env): Recor } return env } + +// ── REQ-137 (`#1337`):sidecar 的代理栈由策略代理**独占**决定 ───────────────────────────── +// 围栏(process-fence-profile.ts 的 N1–N4)把 sidecar 那棵树的 TCP/UDP 出网收成一条:loopback 上 main 进程内的 +// 策略代理(network-egress-proxy.ts)。env 是「汇到哪」的另一半(`#1334` Q4:没有代理 env,seatbelt 只会把流量 +// 掐死,不会把它汇到任何地方)—— 所以八个代理变量在这里**整份改写**,不与用户导出的值合并: +// · 用户自己的 HTTP(S)_PROXY(比如本机 7897 的系统代理)不能留:围栏只放行策略代理那一个端口,指向别处的 +// 代理只会让每一条出网 EPERM,而且 NO_PROXY 也不能带用户的名单 —— 名单里的目的地会绕开代理直连,直连在 +// 围栏里死在 DNS 这一步,表现是 ENOTFOUND 而不是策略拒绝(归因错误)。唯一通路就是唯一通路。 +// · NO_PROXY 只留 loopback 三个名字:sidecar 自己的 HTTP 服务器、本机 ollama 一类 loopback 目的地不经代理 +// (sidecar.ts ensureLoopbackNoProxy 对同一份值幂等)。 +// · ALL_PROXY 一并设(curl / git 认它;node 不认,无害),scheme 显式 http://。 +// createSidecarEnv 的白名单里这八个名字本来就放行(上面 EXACT 的 proxy stack 段),这里改写的是**值**。 + +export const SIDECAR_EGRESS_NO_PROXY = "127.0.0.1,localhost,::1" + +/** 给定策略代理端口,返回要**覆盖**进 sidecar env 的八个代理变量(纯函数;server.ts 在 fork 前 Object.assign)。 */ +export function sidecarEgressProxyEnv(port: number): Record { + if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`sidecar egress proxy env: port must be an integer in 1..65535, got ${JSON.stringify(port)}`) + const url = `http://127.0.0.1:${port}` + return { + HTTP_PROXY: url, + HTTPS_PROXY: url, + ALL_PROXY: url, + http_proxy: url, + https_proxy: url, + all_proxy: url, + NO_PROXY: SIDECAR_EGRESS_NO_PROXY, + no_proxy: SIDECAR_EGRESS_NO_PROXY, + } +} diff --git a/packages/ui-mac/src/main/workspace-write-probe-fence.test.ts b/packages/ui-mac/src/main/workspace-write-probe-fence.test.ts index 6ce505979af0..5e2274bef0c9 100644 --- a/packages/ui-mac/src/main/workspace-write-probe-fence.test.ts +++ b/packages/ui-mac/src/main/workspace-write-probe-fence.test.ts @@ -58,7 +58,8 @@ describeDarwin("AC3 真探针在真围栏下(真 .node / 真 seatbelt / 真子 profileFile = join(scratch, "fence.sb") writeFileSync( profileFile, - renderProcessFenceProfile({ workspaces: [ws], alphaGlobalRoot: globalRoot, userDataPath: userData, stateHome: userData, roots: resolveEngineRoots({}, homedir()) }), + // `#1337`:生产 profile 含网络行;写探针不出网,给一个没人听的端口即可。 + renderProcessFenceProfile({ workspaces: [ws], alphaGlobalRoot: globalRoot, userDataPath: userData, stateHome: userData, roots: resolveEngineRoots({}, homedir()), egressProxyPort: 4443 }), ) }) diff --git a/packages/ui-mac/src/renderer/i18n/en.ts b/packages/ui-mac/src/renderer/i18n/en.ts index 676cf215d85e..ba2cfab88b27 100644 --- a/packages/ui-mac/src/renderer/i18n/en.ts +++ b/packages/ui-mac/src/renderer/i18n/en.ts @@ -1504,8 +1504,11 @@ export const dict = { "alpha.terminal.sandboxFoot": "Sandbox on · some of your shell config may not apply", "alpha.terminal.sandboxFootShort": "Sandbox on", "alpha.terminal.sandboxHoverTitle": "Sandbox on", + // REQ-137 `#1337` (AC5 coverage disclosure): the fourth sentence is about the network — the sandbox covers only the assistant + // engine's process tree (terminal and the commands it spawns included); Code Puppy's own traffic (model catalog, sign-in, + // update checks) is NOT covered. Guarded by src/main/network-egress-disclosure.test.ts. "alpha.terminal.sandboxHoverBody": - "The terminal can only write inside the current project and Code Puppy's own folders. Steps in your shell config that write caches to your home folder (completion caches, for example) are blocked silently — completion may feel slow or stop working. Settings that don't write files (aliases, prompt, PATH) work as usual.", + "The terminal can only write inside the current project and Code Puppy's own folders. Steps in your shell config that write caches to your home folder (completion caches, for example) are blocked silently — completion may feel slow or stop working. Settings that don't write files (aliases, prompt, PATH) work as usual. Network access works the same way: the assistant and the terminal can only reach registered addresses through Code Puppy's egress gate; anything else is refused and logged. Code Puppy's own traffic (model catalog, sign-in, update checks) is outside this sandbox and connects as before.", "alpha.terminal.sandboxEmpty": "Sandbox is on — some of your own shell config may not apply in here.", "alpha.workspace.readonly": "Read-only", "alpha.workspace.readonlyTitle": "This project can't be written to right now", diff --git a/packages/ui-mac/src/renderer/i18n/zh.ts b/packages/ui-mac/src/renderer/i18n/zh.ts index 94b8f2f7e669..a0b788990e82 100644 --- a/packages/ui-mac/src/renderer/i18n/zh.ts +++ b/packages/ui-mac/src/renderer/i18n/zh.ts @@ -1473,8 +1473,10 @@ export const dict = { "alpha.terminal.sandboxFoot": "沙箱开启 · 你的部分终端配置可能不生效", "alpha.terminal.sandboxFootShort": "沙箱开启", "alpha.terminal.sandboxHoverTitle": "沙箱开启", + // REQ-137 `#1337`(AC5 覆盖面如实声明):第四句说联网 —— 这道沙箱只罩助手引擎那棵进程树(含终端与它派生的命令), + // Code Puppy 应用自身(模型目录、登录、检查更新)的联网**不在**其内;判据在 src/main/network-egress-disclosure.test.ts。 "alpha.terminal.sandboxHoverBody": - "终端只能写入当前项目和 Code Puppy 自己的目录。你的 shell 配置里凡是要往主目录写缓存的步骤(例如补全缓存)会被拦下,而且不会报错 —— 表现通常是补全变慢或不灵。不写文件的设置(别名、提示符、PATH)照常生效。", + "终端只能写入当前项目和 Code Puppy 自己的目录。你的 shell 配置里凡是要往主目录写缓存的步骤(例如补全缓存)会被拦下,而且不会报错 —— 表现通常是补全变慢或不灵。不写文件的设置(别名、提示符、PATH)照常生效。联网也一样:助手和终端只能经 Code Puppy 的出网闸门访问已登记的地址,其它地址会被拒绝并留下记录;Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内,照常直连。", "alpha.terminal.sandboxEmpty": "沙箱已开启 —— 终端里你自己的配置可能有一部分不生效。", "alpha.workspace.readonly": "只读", "alpha.workspace.readonlyTitle": "这个项目现在写不进去", diff --git a/packages/ui-mac/test-component/terminal-rail.cases.ts b/packages/ui-mac/test-component/terminal-rail.cases.ts index b41593e5b769..07954276e98b 100644 --- a/packages/ui-mac/test-component/terminal-rail.cases.ts +++ b/packages/ui-mac/test-component/terminal-rail.cases.ts @@ -363,6 +363,8 @@ describe("REQ-159 #1322 sandbox disclosure on the terminal face (AC1)", () => { expect(card?.querySelector("b")?.textContent).toBe("沙箱开启") expect(card?.textContent).toContain("例如补全缓存") expect(card?.textContent).toContain("不写文件的设置(别名、提示符、PATH)照常生效") + // REQ-137 `#1337` AC5:覆盖面声明到达了用户眼前(文案本体由 src/main/network-egress-disclosure.test.ts 守;这里守它上了卡)。 + expect(card?.textContent).toContain("Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内") // 键盘可达(悬停层也能用 focus 打开)。 expect(item!.getAttribute("tabindex")).toBe("0") }) diff --git a/packages/ui-mac/test-fixtures/process-fence/network-driver.mjs b/packages/ui-mac/test-fixtures/process-fence/network-driver.mjs new file mode 100644 index 000000000000..96b9bcb8e064 --- /dev/null +++ b/packages/ui-mac/test-fixtures/process-fence/network-driver.mjs @@ -0,0 +1,158 @@ +// REQ-137 (`#1337`) —— 在**出货 sidecar 的运行时**(Electron 内嵌 node,ELECTRON_RUN_AS_NODE=1)里跑的网络探针: +// 装上生产渲染的 profile(经生产 .node 模块),然后逐条试「唯一放行的那扇门」与逃逸语料。 +// 判据不在这里 —— 这里只做动作并把自报打成 JSON(每条带 errno / 状态码 / 耗时);判在 network-egress-fence.test.ts。 +// +// argv: +// env : 由测试进程按生产 sidecarEgressProxyEnv(port) 注入(HTTP(S)_PROXY / NO_PROXY …),本文件不自设。 +// +// 步骤(每步各自 try/catch,一步失败不影响下一步;`started` 由本进程 pid 自证 —— 空输出 ≠ 拦住): +// listen net.createServer().listen(0,"127.0.0.1"),把端口打成一行 `LISTENING ` 给测试进程, +// 等外面(未被围栏)的客户端连进来并回 SERVED —— 票面硬要求一(bind / inbound 必须放行)。 +// proxyFetch http.setGlobalProxyFromEnv()(= sidecar.ts useEnvProxy 的形状)后 fetch(http://:/): +// 名字只有代理那一侧解得开 ⇒ 200 说明流量真的经代理汇出去了。 +// proxyConnect 裸 TCP 连 127.0.0.1: 发 CONNECT : —— TCP 层的「那扇门」。 +// directTcp net.connect(127.0.0.1:):绕开代理直连靶站 ⇒ 围栏下 EPERM。 +// rawIp443 net.connect(1.1.1.1:443):直连 raw-IP ⇒ EPERM。 +// udp dgram.send 到 1.1.1.1:53 ⇒ EPERM。 +// v6OtherPort net.connect([::1]:):loopback 上**非代理端口** ⇒ EPERM(bare 臂 ECONNREFUSED)。 +// dns dns.lookup("example.com") ⇒ 围栏下解析失败(mDNSResponder 的 unix socket 被 (deny network*) 拦,`#1334` Q2)。 +import { createRequire } from "node:module" +import dgram from "node:dgram" +import dns from "node:dns" +import fs from "node:fs" +import http from "node:http" +import net from "node:net" + +const [addonPath, mode, profileFile, targetName, targetPortRaw, proxyPortRaw, closedV6PortRaw] = process.argv.slice(2) +const targetPort = Number(targetPortRaw) +const proxyPort = Number(proxyPortRaw) +const closedV6Port = Number(closedV6PortRaw) +const out = { runtime: `node ${process.versions.node} (electron ${process.versions.electron ?? "-"})`, mode, pid: process.pid, steps: {} } +const emit = () => console.log(JSON.stringify(out)) +const fail = (why) => { + out.fatal = why + emit() + process.exit(3) +} + +if (mode === "fenced") { + const m = { exports: {} } + process.dlopen(m, addonPath) + const r = m.exports.apply(fs.readFileSync(profileFile, "utf8")) + out.buildId = m.exports.buildId + out.apply = r + if (r.rc !== 0) fail(`apply rc=${r.rc}: ${r.error}`) +} + +const timed = async (fn) => { + const t0 = Date.now() + try { + const value = await fn() + return { ok: true, ...value, ms: Date.now() - t0 } + } catch (e) { + return { ok: false, code: e?.code ?? e?.cause?.code ?? null, message: String(e?.cause?.message ?? e?.message ?? e), ms: Date.now() - t0 } + } +} +const withTimeout = (p, ms, label) => + Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(Object.assign(new Error(`${label} probe timeout ${ms}ms`), { code: "PROBE_TIMEOUT" })), ms))]) + +/** TCP connect 结果:连上 ⇒ {connected:true};错 ⇒ 抛(带 errno)。 */ +const tcp = (host, port) => + withTimeout( + new Promise((resolve, reject) => { + const s = net.connect({ host, port }) + s.once("connect", () => { + s.destroy() + resolve({ connected: true }) + }) + s.once("error", (e) => reject(e)) + }), + 4000, + `tcp ${host}:${port}`, + ) + +// listen —— 先于一切网络探针,因为它是「引擎还起得来吗」这一格(`#1334` Q1.1)。 +out.steps.listen = await timed( + () => + new Promise((resolve, reject) => { + const srv = net.createServer((sock) => { + sock.once("data", (d) => { + sock.end(`SERVED:${d.toString().trim()}`) + srv.close() + resolve({ accepted: 1 }) + }) + }) + srv.once("error", reject) + srv.listen(0, "127.0.0.1", () => { + out.listenPort = srv.address().port + console.log(`LISTENING ${out.listenPort}`) + }) + setTimeout(() => { + srv.close() + reject(Object.assign(new Error("no external client within 8s"), { code: "NO_CLIENT" })) + }, 8000) + }), +) + +// proxyFetch —— 生产 sidecar 的形状:先 setGlobalProxyFromEnv,再用全局 fetch。 +out.steps.proxyFetch = await timed(async () => { + http.setGlobalProxyFromEnv() + const r = await withTimeout(fetch(`http://${targetName}:${targetPort}/`), 6000, "proxyFetch") + return { status: r.status, body: (await r.text()).slice(0, 80) } +}) + +// proxyConnect —— TCP 层那扇门。 +out.steps.proxyConnect = await timed( + () => + withTimeout( + new Promise((resolve, reject) => { + const s = net.connect({ host: "127.0.0.1", port: proxyPort }) + let text = "" + s.once("error", reject) + s.once("connect", () => s.write(`CONNECT ${targetName}:${targetPort} HTTP/1.1\r\nHost: ${targetName}:${targetPort}\r\n\r\n`)) + s.on("data", (c) => { + text += c.toString() + const sep = text.indexOf("\r\n\r\n") + if (sep < 0) return + const status = Number(text.split(" ")[1] ?? 0) + s.destroy() + resolve({ status, head: text.slice(0, sep).split("\r\n")[0] }) + }) + }), + 4000, + "proxyConnect", + ), +) + +out.steps.directTcp = await timed(() => tcp("127.0.0.1", targetPort)) +out.steps.rawIp443 = await timed(() => tcp("1.1.1.1", 443)) +out.steps.udp = await timed( + () => + withTimeout( + new Promise((resolve, reject) => { + const s = dgram.createSocket("udp4") + s.once("error", (e) => { + s.close() + reject(e) + }) + s.send(Buffer.from("ac1337"), 53, "1.1.1.1", (e) => { + s.close() + e ? reject(e) : resolve({ sent: true }) + }) + }), + 4000, + "udp", + ), +) +out.steps.v6OtherPort = await timed(() => tcp("::1", closedV6Port)) +out.steps.dns = await timed( + () => + withTimeout( + new Promise((resolve, reject) => dns.lookup("example.com", (e, address) => (e ? reject(e) : resolve({ address })))), + 6000, + "dns", + ), +) + +emit() +process.exit(0) diff --git a/scripts/gate-files.tsv b/scripts/gate-files.tsv index 80880b12777b..9949d1eb47cf 100644 --- a/scripts/gate-files.tsv +++ b/scripts/gate-files.tsv @@ -113,7 +113,7 @@ 18 packages/ui-mac src/shared/package-secret-prerequisite.test.ts packages/ui-mac/src/main/ext-write-channels.test.ts #696 secret prerequisite:host profile 派生、renderer 篡改无效、cancel/replacement/uninstall/missing/stale fail-closed、既有 secret store 与 main authenticated engine reload、全链零真值回显;`#809` 追加 managed plugin 密钥前置空集的具名用例 + payload 塞 requiredSecrets 解码期即拒(此行 prose 曾写 20 而登记列是 18、实测 18 —— #844 起条数只看登记列) 11 packages/ui-mac src/main/ext-write-channels.test.ts - #696 零真值回显的**行为判据**:起子进程跑 test-component/ext-install-catalog-result.cases.ts,对真 ext-install-catalog IPC 用 canary 密钥装两次,分别覆盖 enabled 与 installedDisabled 两条返回分支,断言返回键属于白名单且序列化结果不含 canary(子进程用例数以 /\b2 pass\b/ 词边界钉死)。删掉本文件 = 该保证消失,源码文本扫描挡不住。 59 packages/ui-mac src/main/ext-receipt-v2.test.ts - 受托:REQ-100 账本 decoder 强度 + 派生锁步(gen-skill-paths 的主判据);`#773` 追加:V3 三条只读路径(图/claim/计划期状态)对同一本坏账本给出同一个诊断 —— 图或 claim 任一条改回 `parseLedger(...).parsed.X` 的静默 `[]` 即红(空账本仍须 ok:true,防「永远失败」也能全绿) -30 packages/ui-mac src/main/sidecar-env.test.ts - 受托:sidecar env 白名单真实判据(websearch-copies 的 env 半场) +31 packages/ui-mac src/main/sidecar-env.test.ts - `#1337` +1:sidecarEgressProxyEnv 八个代理变量整份给出(六个指向 loopback 策略代理、NO_PROXY 只留 loopback)、八个名字都在白名单里、叠在 createSidecarEnv 之上时用户导出的代理 / NO_PROXY 不存活、坏端口即抛。受托:sidecar env 白名单真实判据(websearch-copies 的 env 半场) 13 packages/ui-mac src/main/alpha-config-injection.test.ts packages/ui-mac/src/main/sidecar-ready-message.test.ts,packages/ui-mac/src/main/alpha-models.test.ts 受托:配置注入真实判据(websearch-copies 的注入半场);「失败值真的上车」与单件 model 组装再委派;#857 钉住治理启用时从既有 provider 投影机械生成最小 upstream base、缺少用户文件 provider 投影时不放 early marker、关闭治理时保留 operator 的 OPENCODE_MODELS_PATH escape hatch 13 packages/ui-mac src/main/config-injection-throat.test.ts - REQ-157 #1305 ui-mac 交给引擎的整份 OPENCODE_CONFIG_CONTENT 的一道咽喉(父票 #1284 AC1 的收口;#1296 的 instructions 咽喉与 #1299 的 agent 咽喉已并入本文件,不再各自成道)的唯一执行者:跑真 injectAlphaConfig,hook 之后新写入/改动的每个字符串叶子按来源先排除用户的字(继承 config 里没被动过的叶子 / REQ-063 导入目录按目录放行),其余必须是 packages/ext/src/context-injection.ts 同一份登记簿的登记文字(instruction 文件正文 / agent prompt|description 按 sink 逐字)、登记簿声明过的引用子树(/$schema /plugin /enabled_providers /model /provider /mcp;只认引用不认文字)、或引擎动词(agent mode / agent permission.** / 顶层 permission.**,集合与引擎源码字面量逐字锁死),其它一律点名 —— 不是 pointer 白名单。全栈 env(ext bundle + 平台/BYOK 密钥 + 默认模型 + 云 MCP 代付)下九个顶层键全部真被写到、生产走到的引用集合与本文件点名的 ui-mac 引用双向相等(死引用即红);8 组 env identity 形状集合与登记簿变体双向相等;每个判据带已知的坏,最要紧的一条打在判官从没见过的键上(往 cfg.command.* 写登记过的 behavior 正文 / 一句 prose / 一个未声明顶层键 ⇒ 三处点名)。变异实测:生产多写一处 config.command 当场 2 pass / 11 fail 并点名 /command//template 与 /description。删掉它,ui-mac 主进程往任何键写 alpha 的字都不会有测试变红。精确条数=实测 13,不留余量 4 packages/ui-mac src/main/sidecar-ready-message.test.ts - 受托:buildReadyMessage 运行时闸 —— 纯文本锚曾被变异实证绕过,值上车的判据在这里#881:prewarm 的 outcome/耗时/status **成功路径也必须带**(只在失败时带 ⇒ 打包全绿那次反而无从归因),无 status 的结局不得凭空长出一个;两支夹具三值互异 ⇒ 写死常量至少在一支上红。 @@ -266,14 +266,16 @@ 9 packages/ui-mac src/main/sync-upstream-merge-order.test.ts - `#1272`:`.github/workflows/sync-upstream.yml` 里「Merge `dev` into `alpha`」那一步的**行为闸** —— 它守的是**顺序**:冲突分支必须先结束合并、再 `apply_alpha_frontend_delta`(重贴 alpha 前端 SOT 补丁)、**最后**才 `bun install`。为什么这条不变量需要一道门:ADR-034 起 packages/{app,ui} 是「pin + 补丁」的投影,而 packages/session-ui 以 `file:../app/vendor/*.tgz` **直接依赖**一个只存在于补丁里的二进制(pin 里没有 packages/app/vendor;2026-09-06 实测 `git ls-tree 849c2598 packages/app/vendor` 输出为空)⇒ 补丁没贴就 install = 资产不在树上 = 必然 `failed to resolve`。修复前的冲突分支正是这个顺序,于是那条路径**结构性不可用**,而 2026-09-06 `ac#1248` 手工复演时才被撞出来(原文 `@opencode-ai/client@file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz failed to resolve`)。更贵的一点:同一份 workflow 里 `VENDORED` 那条 loud-fail **正是为这一格写的**(它会指向「须用 git diff --binary 重生」/月更 bump),顺序错时它永远来不及打印 —— 判据是对的,人却只拿到 bun 的天书。为什么判据必须是行为的:断言 YAML 源码文本(「apply 排在 install 前面」)在本仓是点名过的假闸门形态 —— 有人在更靠前的地方再加一句 install、或把 install 整个删掉(那样 alpha 会带着上游那份不含自己 workspace 的 lockfile 被推走),文本判据都照样绿。本文件用 `Bun.YAML` 从生产 workflow 解析出**那一步的 run 体**,在真 git 仓(pin / alpha delta / SOT 补丁 / 上游前进 四段历史齐备)上用 `bash -e` 跑它本体,并用一个记录「每次调用发生时资产在不在树上」的假 `bun` 观测。九条各钉一个方向:①冲突 + 健康补丁 ⇒ exit 0 且 install 那一刻资产 PRESENT(缺这条对照组,一个恒红空壳能满足下面全部红向断言);②**变异臂**——把生产 body 的 `apply_alpha_frontend_delta`/`bun install` 两行换回旧顺序 ⇒ 同一夹具当场 MISSING + `failed to resolve` 且**一句 `::error::` 都没有**(先证明这个手段能测出已知的坏,再用它判未知的好;变异没改到字节就抛「测量作废」);③冲突 + `file:` 换名漂移 ⇒ 以 workflow 自己那句可读 loud-fail 失败、点名到精确文件、且 install **一次都没跑**;④变异臂 + 同一漂移 ⇒ 复现 2026-09-06 撞到的那一格(`failed to resolve`,可读原因永不打印);⑤冲突分支必须**重生并提交** bun.lock(杀掉「把 install 删掉了事」的修法,并证明 `--theirs` 那一格真的生效);⑥无冲突分支没被改坏:exit 0、补丁重贴、且**不跑** install(它在后面的 Engine smoke 步);⑦无冲突 + 同一漂移 ⇒ 两条分支给出同一句可读 loud-fail(人不该因为走了哪条分支而看见两种话);⑧意外冲突(落在 app/ui 之外)⇒ 点名、abort、exit 1、install 零调用;⑨**减速带**(已标诚实边界):合并步**之前**的任何一步都不许 `bun install` —— 上面八条只跑那一步,「有人在更靠前的步骤里加了 install」这个方向行为闸罩不到,而它会让本票的缺陷原样复活。主判据**不依赖假 bun 的保真度**:它判的是日志里的 MISSING/PRESENT,而「资产缺席时 install 会失败」由真 bun 单独证过(2026-09-06 本机 bun 1.3.14:`error: @opencode-ai/client@file:…tgz failed to resolve`,exit 1)。删掉本文件:那一步退回零行为判据 —— 顺序换回去、install 被删、VENDORED loud-fail 被吞、no-conflict 分支被改成也跑 install,都不会有任何东西变红(那条 workflow 只在 GitHub 上、且只在有冲突时才走到这几行)。精确条数 = 实测 9,不留余量 6 packages/ui-mac src/main/embedded-server-version.test.ts - ac#1248(A4 回归):上游 e11dbd020 起 build-node.ts define 了 OPENCODE_VERSION,embedded server bundle 里 InstallationVersion 变成烤死的字面量(alpha 构建下恒为 0.0.0-- 或打包任务导出的 app 版本),npm 上没有同号的 @opencode-ai/plugin ⇒ config.ts 的后台依赖安装恒失败。本闸钉住 scripts/patch-server-version.ts:①把那一个字面量改成真实 npm 版本且不碰别处;②app 版本同样替换;③幂等;④旧形状(typeof … : "local")与 0/多个命中一律抛错(fail-closed,旧版脚本在这里静默 no-op 并 exit 0);⑤目标版本必须是发布号。夹具逐字取自同步栈真构建。 4 packages/ext src/alpha-ext-no-shell-layer.test.ts - REQ-159/#1321 AC2「REQ-138 层已拆」的唯一执行者:跑**生产的** AlphaExt config 钩子,断言 cfg.shell 前后逐字相同(不包、不替换、不回落 deny stub)、引擎 env 不再多出 ALPHA_SB_PROFILE / ALPHA_REAL_SHELL、global root 下不再产出 bin/ 与 sandbox/。两层并存实测 sandbox_apply: Operation not permitted、HTTP 200 而零执行(勘破 §6.5/§8.4)—— 谁把 wrapper 加回来,shell 工具整个废掉而接口报成功,而 ext 地板 100 对实测 200+ 有余量,不登记就可以整删 -17 packages/ui-mac src/main/process-fence-profile.test.ts - REQ-159/#1321 可写集**单一权威的形状**:renderProcessFenceProfile 的输出与勘破 §8.2 的 19 行逐行全等(少一行 / 放宽一行即红);XDG 根对着 node_modules 里那份 xdg-basedir(core/global.ts 真 import 的)逐输入交叉验证;并集裁剪规则(默认工作区首位 → recent → tab 栏 → info;K=32;排除 / HOME 及祖先;不存在不收;去重)按顺序相等判;试编译封顶从尾部丢、最小集仍失败必抛且带编译器原文。全平台纯逻辑 +21 packages/ui-mac src/main/process-fence-profile.test.ts - REQ-159/#1321 + REQ-137/#1337 可写集与网络行**单一权威的形状**:`#1337` 加四条 —— N1–N4 网络行逐字等于 `#1334` Q1.2(deny 在前、只写加法、N4 端口 = 传入的代理端口、没有 mDNSResponder 行;少 N3 / 多一条减法 deny 即红)、代理端口非 1..65535 整数即拒渲染、trimUntilCompiles 的归因(非字节墙的失败一个工作区都不丢即抛并点名)。原有:renderProcessFenceProfile 的输出与勘破 §8.2 的 19 行逐行全等(少一行 / 放宽一行即红);XDG 根对着 node_modules 里那份 xdg-basedir(core/global.ts 真 import 的)逐输入交叉验证;并集裁剪规则(默认工作区首位 → recent → tab 栏 → info;K=32;排除 / HOME 及祖先;不存在不收;去重)按顺序相等判;试编译封顶从尾部丢、最小集仍失败必抛且带编译器原文。全平台纯逻辑 4 packages/ui-mac src/main/process-fence-compile.test.ts - REQ-159/#1321 试编译这台仪器先自证:已知会撞 65535 字节墙的 profile(400 条互不相同的 220 字符路径)必须判 ok:false 且原因含 exceeds maximum;语法坏的必须判 ok:false;§8.2 形状 + 32 个工作区必须判 ok:true;编译器跑不起来必须说 could not run 而不是 ok。真 /usr/bin/sandbox-exec。[平台:darwin] 非 darwin 上文件自报 skip,由本登记簿按「自报 0 条」验证标注 -8 packages/ui-mac src/main/process-fence-plan.test.ts - REQ-159/#1321 main 侧计划器的接线:父目录 W4/W5/W6/W7 预建(勘破 §6.3.1 第 3 条,不建 W16/W10/W17/W18)、XDG_STATE_HOME 跟着 sidecar.ts 的决定、store 读挂不炸 boot 但出声、试编译丢尾并在日志点名、最小集编不过 / 原生模块不在 / 默认工作区不在 三种 fail-closed 各自抛且原因可读、dev 与打包两种 addonPath 解析。[平台:darwin] 非 darwin 上文件自报 skip +10 packages/ui-mac src/main/process-fence-plan.test.ts - REQ-159/#1321 + REQ-137/#1337 main 侧计划器的接线:`#1337` 加两条、改写一条 —— 正向多断言计划里的 profile 带 N4(传入的代理端口)且日志点名 egressProxyPort;fail-closed ① 改写成「非字节墙的编译失败 0 dropped / 1 attempt 即抛并点名不是并集大小」,新增 ①′「字节墙丢到只剩默认工作区仍失败抛最小可写集」与 ④「代理端口不合法不产计划」。原有:父目录 W4/W5/W6/W7 预建(勘破 §6.3.1 第 3 条,不建 W16/W10/W17/W18)、XDG_STATE_HOME 跟着 sidecar.ts 的决定、store 读挂不炸 boot 但出声、试编译丢尾并在日志点名、最小集编不过 / 原生模块不在 / 默认工作区不在 三种 fail-closed 各自抛且原因可读、dev 与打包两种 addonPath 解析。[平台:darwin] 非 darwin 上文件自报 skip 11 packages/ui-mac src/main/process-fence-apply.test.ts - REQ-159/#1321 围栏本体:真 .node(用生产构建脚本现编)、真 seatbelt、**Electron 内嵌 node**(= utilityProcess 的运行时)。A:x64 判据 —— 生产构建同时有 arm64+x86_64 两片且各自导出 napi_register_module_v1,先拿只编 arm64 的 thin 文件与一个 fat 空壳证明判据会红(票面硬要求四);B:AC1 四类派生原语(shell / 孙进程 / detached / PTY 缺省与带 command)在生产渲染的 profile 下界外 0 落盘、界内落盘,同一探针不套围栏 6/6 落盘;C:AC4 五种失败经生产 applyProcessFence 各自抛 ProcessFenceError 且原因可读(编译失败带 libsandbox 原文 / 缺 deny / 模块不在 / rc=0 但集合外仍写得进=围栏是空的 / cwd 写不进);D:成功回报 buildId=本次编译。[平台:darwin] 非 darwin 上文件自报 skip -5 packages/ui-mac src/main/process-fence-wiring.test.ts - REQ-159/#1321 main 侧三跳:生产 spawnLocalServer 把计划器给出的 profile+addonPath 逐字放进线上 start 命令;计划器抛出 ⇒ spawn 拒绝、消息带原因、fork 一次都不发生;计划器回 undefined ⇒ darwin 拒 fork / 其它平台照常且不带 fence。末条是 sidecar.ts 的接线锚(顶层 registerHooks/getParentPort 让它无法 import):installProcessFence 早于注入与 import 引擎、darwin 缺席即抛 +8 packages/ui-mac src/main/process-fence-wiring.test.ts - REQ-159/#1321 + REQ-137/#1337 main 侧三跳 + 网络三跳:`#1337`(darwin)—— 策略代理先于计划起、端口进计划器且 fork 的 env 八个代理变量整份改写指向它(用户自己的 HTTPS_PROXY / NO_PROXY 不存活,main 的 process.env 一字不动;非 darwin 不起代理 env 照旧)、代理起不来 ⇒ 拒 fork 零 fork 零计划、缺省提供者真起监听且跨代同一端口并只做 CONNECT(明文 GET 405)。原有:生产 spawnLocalServer 把计划器给出的 profile+addonPath 逐字放进线上 start 命令;计划器抛出 ⇒ spawn 拒绝、消息带原因、fork 一次都不发生;计划器回 undefined ⇒ darwin 拒 fork / 其它平台照常且不带 fence。末条是 sidecar.ts 的接线锚(顶层 registerHooks/getParentPort 让它无法 import):installProcessFence 早于注入与 import 引擎、darwin 缺席即抛 12 packages/ui-mac src/main/process-fence-write-sites.test.ts - REQ-159/#1321 AC3 可写集单一权威的另一半:alpha 在 sidecar 进程里的每一处写盘调用点(ext/src 全部 + sidecar.ts 相对 import 闭包,TypeScript AST 解析 fs 绑定)都登记在 scripts/process-fence-write-sites.tsv 并点名落在 W-id 哪一行;未登记 / 已失效 / 根写歪 / TODO 任一即红。四条控制臂:未登记 appendFileSync 必抓、`writeFileSync as sneaky` 改名必抓、namespace+promises 必抓、多行 import(勘破 §7.3 正则漏掉的那一形)必抓;非 fs 同名方法不算;集合自证含 sidecar 真 import 的模块、不含 index.ts 与测试文件。全平台 4 packages/ui-mac src/main/process-fence-engine.test.ts - REQ-159/#1321 AC1+AC2 的**真消费方**判据:真引擎(bun, dev 树)+ 真 @alpha-code/ext + 生产 planProcessFence(真试编译)+ 生产 .node 在引擎装载任何模块前 sandbox_init(bun --preload);shell 工具端点 / POST /pty 缺省与带 command / POST /mcp 经 MCP SDK 起真 stdio server 三条真消费方界内落盘、界外 0 落盘,同一工作负载不套围栏 3/3 落盘;AC2:围栏下 shell 工具照常执行(zsh 自己报 operation not permitted 且命令跑到最后一句,不是 sandbox_apply 零执行),GET /config 的 shell 不指向 REQ-138 wrapper。未闭合:出货 node sidecar + 打包产物的合成(U3 VERIFY 票)。[平台:darwin] 非 darwin 上文件自报 skip -4 packages/ui-mac src/main/network-egress-registry.test.ts - REQ-137/#1336 AC2 目的地授权集**唯一权威的形状与初值**:EGRESS_REGISTRY 逐 host:port 等于勘破 2026-08-25 §2.2 清单(按今日代码坐标校正的独立字面量,多一行 / 少一行 / 改一个域名即红);每行 host 小写无通配无尾点、port 1..65535、类别与出处非空、无重复;平台四族跟着 shared/alpha-config 的 ALPHA_ENDPOINTS 走(不自持第二份域名);isEgressAuthorized 精确 host:port、端口是键、大小写归一、尾点 / 子域 / scheme / 越界端口一律拒(改成恒答 true 当场红)。全平台纯逻辑 -12 packages/ui-mac src/main/network-egress-proxy.test.ts - REQ-137/#1336 AC2 全部 + AC1 策略半场,真代理(生产 startEgressPolicyProxy)+ 真 TCP 靶站:正臂 —— 夹具注册表放行 localhost:<靶站> ⇒ 200 隧道、字节双向到达、名字由代理解析(Happy Eyeballs)、客户端挂断切断上游、tunnel-closed 记录带字节数;默认 authorize 就是注册表(127.0.0.1:11434 走到拨号、未登记端口走不到);**反向** —— 生产注册表下 CONNECT 未登记 loopback 靶站 ⇒ 403 + 可识别正文 + 结构化记录 reason=unregistered + 靶站 0 连接(记录自证代理收到了请求);未登记公网名 ⇒ 403 且零 DNS / 零拨号;**控制臂** —— 注册表换成恒答允许的替身,同一判据函数必须判红并点名替身把请求放到了靶站;非 CONNECT 405 / 坏 authority 400(不查表不拨号)/ 登记了但 ECONNREFUSED、ENOTFOUND、黑洞超时各 502 dial-failed(与 403 可辨);close() 切断在途隧道并释放端口。全平台 +5 packages/ui-mac src/main/network-egress-registry.test.ts - REQ-137/#1336 AC2 目的地授权集**唯一权威的形状与初值**:EGRESS_REGISTRY 逐 host:port 等于勘破 2026-08-25 §2.2 清单(按今日代码坐标校正的独立字面量,多一行 / 少一行 / 改一个域名即红);每行 host 小写无通配无尾点、port 1..65535、类别与出处非空、无重复;平台四族跟着 shared/alpha-config 的 ALPHA_ENDPOINTS 走(不自持第二份域名);isEgressAuthorized 精确 host:port、端口是键、大小写归一、尾点 / 子域 / scheme / 越界端口一律拒(改成恒答 true 当场红)。本机目的地(loopback)不得登记 —— owner 2026-09-10 裁决,加回一行 loopback 即红并点名理由。全平台纯逻辑 +12 packages/ui-mac src/main/network-egress-proxy.test.ts - REQ-137/#1336 AC2 全部 + AC1 策略半场,真代理(生产 startEgressPolicyProxy)+ 真 TCP 靶站:正臂 —— 夹具注册表放行 localhost:<靶站> ⇒ 200 隧道、字节双向到达、名字由代理解析(Happy Eyeballs)、客户端挂断切断上游、tunnel-closed 记录带字节数;默认 authorize 就是注册表(github.com:443 走到拨号、同 host 的 22 端口走不到);**反向** —— 生产注册表下 CONNECT 未登记 loopback 靶站 ⇒ 403 + 可识别正文 + 结构化记录 reason=unregistered + 靶站 0 连接(记录自证代理收到了请求);未登记公网名 ⇒ 403 且零 DNS / 零拨号;**控制臂** —— 注册表换成恒答允许的替身,同一判据函数必须判红并点名替身把请求放到了靶站;非 CONNECT 405 / 坏 authority 400(不查表不拨号)/ 登记了但 ECONNREFUSED、ENOTFOUND、黑洞超时各 502 dial-failed(与 403 可辨);close() 切断在途隧道并释放端口。全平台 +4 packages/ui-mac src/main/network-egress-fence.test.ts - REQ-137/#1337 AC1 强制半场 + AC4:真 .node(生产构建脚本现编)、真 seatbelt、真策略代理(生产 startEgressPolicyProxy,授权 / 拨号为测试注入只放行一个名字)、**Electron 内嵌 node**,profile 由生产渲染器渲染(Q1.2 四行 + 可写集)、env 由生产 sidecarEgressProxyEnv 给出。A0 控制臂(不套围栏)探针读得到通;A1 围栏 + 代理在场:apply rc=0、能 listen 且外面连得进(硬要求一)、fetch 与裸 CONNECT 都经代理到达只有代理解得开的名字、逃逸语料(绕代理直连 / raw-IP:443 / UDP / [::1] 其它端口)逐条 EPERM、DNS 不通,每条带 pid 与耗时;A2 同一判据对 bare 臂必红并点名;A3 代理关掉后 fetch / CONNECT 立刻 ECONNREFUSED 且点名代理地址(不是探针超时 / ETIMEDOUT),直连仍 EPERM、监听不受影响。耗时只打印不断言。[平台:darwin] 非 darwin 上文件自报 skip +4 packages/ui-mac src/main/network-egress-disclosure.test.ts - REQ-137/#1337 AC5 覆盖面如实声明:终端「沙箱开启」悬停卡 zh / en 与勘破文档锚点段落三处都要逐字含「只罩引擎那棵树 / 已登记地址 / 其它地址被拒 / 应用自身联网(模型目录、登录、更新)不在其内」的独立字面量,且不许出现「全部 / 所有出网都被限制」一类过度声明;反向 ①把声明改成全部出网受限 ⇒ 同一判据红并点名过度声明,反向 ②只删掉排除句 ⇒ 红并点名缺哪一句,锚点整段删除 ⇒ 红。全平台纯逻辑 14 packages/ui-mac src/main/workspace-write-probe.test.ts packages/ui-mac/src/main/workspace-write-probe-fence.test.ts REQ-159/#1322 AC2/AC3 工作区写探针的全平台一半:main↔sidecar 合同形状 fail-closed(命令/应答不认形就当没有)、errno 分类只认 EPERM 为围栏的拒绝(EACCES/ENOENT/ENOTDIR 都 unknown,不替围栏背书)、真文件系统上写-删不留残且 wx 不覆盖用户文件、main 侧请求簿(超时 / 子进程退出 / 无关消息 / 对不上号 / postMessage 抛 都答 unknown,不猜);AC3 判据(workspace-write-probe-judge.ts)先自证 —— 恒答「不可写」的替身、恒答「可写」的替身各被拒在一边。真围栏臂委派给 fence 那份。下界=实际条数 14,不留余量 2 packages/ui-mac src/main/workspace-write-probe-fence.test.ts - REQ-159/#1322 AC3 真探针在真围栏下:真 .node(生产构建脚本现编)+ 真 seatbelt + 真子进程(bun)。围栏臂(= sidecar 形态):集合内 writable、集合外 denied(EPERM)、不存在的目录 unknown、两边不留探针文件、pid ≠ 测试进程、buildId = 本次编译,判据过;bare 臂(= main 进程形态,不套围栏):集合外也答 writable ⇒ 判据把它当假探针拒掉 —— 「在 main 里 fs.writeFile 探测恒答可写」那一格的实测。[平台:darwin] 非 darwin 上文件自报 skip,由本登记簿按「自报 0 条」验证标注 diff --git a/scripts/module-size-ratchet.tsv b/scripts/module-size-ratchet.tsv index fbd859650b2a..1fdaa08c0a98 100644 --- a/scripts/module-size-ratchet.tsv +++ b/scripts/module-size-ratchet.tsv @@ -26,4 +26,4 @@ # 声明「这个文件/目录我们盯住了,它只许变小」。 3525 file packages/ui-mac/src/main/ext-install-planner.ts 扩展安装计划器。全仓最大的非测试 .ts 之一(3,525 行),`#1289` 票面点名。它把「解析清单 → 校验 → 排布安装步骤 → 冲突处置」全放在一个文件里;拆分方向是按阶段切模块,各自带自己的类型与用例。拆分本身另立票(本票 out of scope 明写不拆)。 2825 file packages/ui-mac/src/main/ext-transaction.ts 扩展安装事务。2,825 行,`#1289` 票面点名。事务的「准备 / 落盘 / 回滚 / 收据」四段各自都够一个模块;今天挤在一个文件里,改一次要读全篇。拆分另立票。 -55592 tree packages/ui-mac/src/main 桌面端主进程整层。`#1289` 票面的第三个坐标(票面写 56,640,本口径实测 53,539 —— 见抬头)。它是本棘轮里唯一挡得住「把行搬进新文件」的一行:两条 file 行都能靠新开文件绕过,总量不能。 `#1287` 把基线从 53,539 抬到 53,553(+14):`package-installability.ts` 要把签名信封的上架呈现段逐字转发到安全视图(两个可选形参 + 三处调用点 + 说明)。这 14 行**没有更小的写法** —— 少了它们,呈现段解出来就停在 main 里,渲染层拿不到,AC1「Hub 呈现面从该段取值」结构上做不到;搬进新文件只会让 tree 这一行照样涨,那正是本行存在的理由。 `#1321`(REQ-159)把基线从 53,553 抬到 54,542(+989):进程围栏落地 —— 可写集单一权威(process-fence-profile.ts)、真编译器试编译(process-fence-compile.ts)、sidecar 侧 apply + 双向自证(process-fence-apply.ts)、main 侧计划器(process-fence-plan.ts)、AC3 写盘登记簿扫描器(process-fence-write-sites.ts),以及 server.ts / sidecar.ts 的接线。这是一道新的安全控制,不是把旧代码搬进新文件;拆掉的 REQ-138 那层(226 行)在 packages/ext,不在本树。五个模块各自可单测,合成一个文件只会更难审。 `#1300` 把基线从 54,542 抬到 55,006(+464):墙钟上界断言的枚举器 + 登记簿判官(wall-clock-assertions.ts,462 行;TypeScript AST 三轴扫描、处置合法性、登记簿 diff/渲染)与 sidecar-location-prewarm.ts 可注入 `now` 的 2 行。这是一道新的仓库级棘轮,不是把旧代码搬进新文件;与 process-fence-write-sites.ts 同形(仓里 AST 扫描器的先例住在这一层),拆成两个文件只会让扫描器与判官的契约跨文件。 `#1336`(REQ-137)把基线从 55,006 抬到 55,592(+586,其中 +339 是本票的两个新模块;另外 +247 在 origin/alpha@3197acf 上就已超基线 —— 棘轮按设计只响不拦,前几张票没抬,本票一并抬到实测值):网络出网的策略层落地 —— 授权目的地注册表(network-egress-registry.ts,单一权威,每行带出处坐标)与 loopback CONNECT 策略代理(network-egress-proxy.ts:authority 解析、查表、代理侧 DNS 拨号、四种失败形态各自可辨的结构化记录、隧道生命周期)。这是一道新的安全控制(`#1073` AC2 的咽喉点),不是把旧代码搬进新文件;两个模块分开是因为「注册表是唯一权威」这条判据要能单独对着数据立闸,与 process-fence-profile.ts / process-fence-apply.ts 的分法同形。 +55726 tree packages/ui-mac/src/main 桌面端主进程整层。`#1289` 票面的第三个坐标(票面写 56,640,本口径实测 53,539 —— 见抬头)。它是本棘轮里唯一挡得住「把行搬进新文件」的一行:两条 file 行都能靠新开文件绕过,总量不能。 `#1287` 把基线从 53,539 抬到 53,553(+14):`package-installability.ts` 要把签名信封的上架呈现段逐字转发到安全视图(两个可选形参 + 三处调用点 + 说明)。这 14 行**没有更小的写法** —— 少了它们,呈现段解出来就停在 main 里,渲染层拿不到,AC1「Hub 呈现面从该段取值」结构上做不到;搬进新文件只会让 tree 这一行照样涨,那正是本行存在的理由。 `#1321`(REQ-159)把基线从 53,553 抬到 54,542(+989):进程围栏落地 —— 可写集单一权威(process-fence-profile.ts)、真编译器试编译(process-fence-compile.ts)、sidecar 侧 apply + 双向自证(process-fence-apply.ts)、main 侧计划器(process-fence-plan.ts)、AC3 写盘登记簿扫描器(process-fence-write-sites.ts),以及 server.ts / sidecar.ts 的接线。这是一道新的安全控制,不是把旧代码搬进新文件;拆掉的 REQ-138 那层(226 行)在 packages/ext,不在本树。五个模块各自可单测,合成一个文件只会更难审。 `#1300` 把基线从 54,542 抬到 55,006(+464):墙钟上界断言的枚举器 + 登记簿判官(wall-clock-assertions.ts,462 行;TypeScript AST 三轴扫描、处置合法性、登记簿 diff/渲染)与 sidecar-location-prewarm.ts 可注入 `now` 的 2 行。这是一道新的仓库级棘轮,不是把旧代码搬进新文件;与 process-fence-write-sites.ts 同形(仓里 AST 扫描器的先例住在这一层),拆成两个文件只会让扫描器与判官的契约跨文件。 `#1336`(REQ-137)把基线从 55,006 抬到 55,592(+586,其中 +339 是本票的两个新模块;另外 +247 在 origin/alpha@3197acf 上就已超基线 —— 棘轮按设计只响不拦,前几张票没抬,本票一并抬到实测值):网络出网的策略层落地 —— 授权目的地注册表(network-egress-registry.ts,单一权威,每行带出处坐标)与 loopback CONNECT 策略代理(network-egress-proxy.ts:authority 解析、查表、代理侧 DNS 拨号、四种失败形态各自可辨的结构化记录、隧道生命周期)。这是一道新的安全控制(`#1073` AC2 的咽喉点),不是把旧代码搬进新文件;两个模块分开是因为「注册表是唯一权威」这条判据要能单独对着数据立闸,与 process-fence-profile.ts / process-fence-apply.ts 的分法同形。 `#1337`(REQ-137)把基线从 55,592 抬到 55,726(+134,全部是本票的:`#1336` 那一行已把此前几张票遗留的 +247 一并抬进实测值,本票起点 = 干净 origin/alpha@59b26f1 的 55,592):网络出网的强制半场接线 —— process-fence-profile.ts 多 Q1.2 四行网络规则的渲染、端口断言与 trimUntilCompiles 的归因分支;process-fence-plan.ts 多代理端口的透传;sidecar-env.ts 多 sidecarEgressProxyEnv(八个代理变量的唯一写法);server.ts 多策略代理单例(起 / 复用 / 失败即拒 fork)与 env 改写;network-egress-registry.ts 多 `#1073` 裁决二那一行(release-assets.githubusercontent.com:443)及其出处注。这是把 `#1336` 那道安全控制**接通**的最小接线,不是把旧代码搬进新文件;四处各自在既有模块里是因为它们分别是那四个模块已有职责(渲染 profile / 做计划 / 决定 sidecar env / fork 前的 fail-closed 序列)的延伸。