Skip to content

[ac#1337] 把路封死:围栏只放行 main 进程内策略代理那一个端口,并如实声明覆盖面 - #1339

Merged
jinjunnn merged 2 commits into
alphafrom
feat/1337-egress-fence-wiring
Sep 11, 2026
Merged

[ac#1337] 把路封死:围栏只放行 main 进程内策略代理那一个端口,并如实声明覆盖面#1339
jinjunnn merged 2 commits into
alphafrom
feat/1337-egress-fence-wiring

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #1337
Refs #1073

REQ-137 的强制半场:围栏的网络面从 (allow default) 变成「只放行 loopback 上策略代理那一个端口」,代理在 Electron main 进程内起、跨 respawn 复用,sidecar 的八个代理变量由 main 整份改写指向它。负责父票 AC1 强制半场 · AC3 · AC4 · AC5;#1336 的代理本体与注册表没改(只加了 owner 裁决二那一行)。

改了什么(每一行都能在树上指到)

文件 事实
profile(强制层) packages/ui-mac/src/main/process-fence-profile.ts 可写集之后追加 #1334 Q1.2 那四行逐字(N1 (deny network*) · N2 network-bind localhost:* · N3 network-inbound localhost:* · N4 network-outbound localhost:<port>);egressProxyPort 成为 ProcessFenceProfileInput 的必填项,非 1..65535 整数即拒渲染;只写加法,DNS 刻意不放行
归因(硬要求四) 同文件 trimUntilCompiles 编译失败若不是 exceeds maximum 那道字节墙 ⇒ 一个工作区都不丢当场抛,消息点名「dropping workspaces cannot fix it, so none were dropped」;字节墙照旧丢尾
计划 process-fence-plan.ts 透传端口进渲染;process fence planned: 日志多 egressProxyPort=<n>
代理生命周期 + 接线 server.ts ensureEgressPolicyProxy / spawnLocalServer darwin:fork 之前先起(或复用)策略代理 → 端口进计划 + Object.assign(sidecarEnv, sidecarEgressProxyEnv(port)) → 再计划围栏;代理起不来 ⇒ 拒 fork(与围栏计划失败同一条 fail-closed 路);代理记录 network egress {json} 进 main 日志(deny 走 warn)。非 darwin:不起代理、env 照旧(没有强制层的平台不装策略层)
sidecar env sidecar-env.ts sidecarEgressProxyEnv HTTP(S)_PROXY / ALL_PROXY(大小写)= http://127.0.0.1:<port>,NO_PROXY / no_proxy = 127.0.0.1,localhost,::1 —— 用户自己的代理与 NO_PROXY 名单不存活(唯一通路);main 的 process.env 一字不动
注册表(裁决二) network-egress-registry.ts +release-assets.githubusercontent.com:443(出处:#1334 Q4 实拍,shell 工具子进程;本票出货复跑第一轮它被 403 ×2,加表后 allow ×1)。example.com 按裁决一不加,AC3 语料改用 registry.npmjs.org
AC5 产品面 renderer/i18n/{zh,en}.ts alpha.terminal.sandboxHoverBody 终端「沙箱开启」悬停卡多第四句:助手和终端只能经出网闸门访问已登记地址,其它被拒并留记录;Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内
AC5 文档面 docs/architecture/2026-09-10-network-egress-on-process-fence.md §落地 + §覆盖面声明(锚点段落) 落地形状表 + 覆盖面声明;2026-09-09-req159-process-fence.md §7 与 2026-08-25-network-egress-seam.md §2.2 落地记录同步改指向

四条 AC 各自的判据与反向用例实际输出

AC1 强制半场 —— network-egress-fence.test.ts(4 条,darwin:真 .node + 真 seatbelt + 真代理 + Electron 内嵌 node)

profile 由生产渲染器渲染(Q1.2 四行 + 可写集),env 由生产 sidecarEgressProxyEnv 给出,靶站是只有代理那一侧解得开的名字。判据实际输出:

[ac1337 A1 fenced/up] pid=17521 listen=ok(1,11ms) proxyFetch=ok(200,16ms) proxyConnect=ok(200,1ms) directTcp=EPERM(0ms) rawIp443=EPERM(1ms) udp=EPERM(0ms) v6OtherPort=EPERM(0ms) dns=ENOTFOUND(2ms)
  • 硬要求一(bind / inbound):被围栏的进程 listen() 成功且本进程(围栏外)连进去拿到 SERVED:ping
  • 唯一那扇门:http.setGlobalProxyFromEnv() 后的 fetch 与裸 CONNECT 都 200 到达靶站;代理记录里 ≥2 条 allow。
  • 逃逸语料:绕代理直连靶站 / raw-IP 1.1.1.1:443 / UDP 1.1.1.1:53 / [::1]:<其它端口> 逐条 EPERM,DNS ENOTFOUND,每条带 pid 与耗时。
  • 反向(A2):同一个判据函数对 bare 臂(不套围栏)判红 —— directTcp should be EPERM, got {"ok":true,"connected":true,...}; udp should be EPERM ...。bare 臂读数:直连连上、UDP 发出、DNS 解出 198.18.7.158
  • 形状闸(process-fence-profile.test.ts +4):N1–N4 逐字等于独立字面量、deny 在前 allow 在后、N4 端口 = 传入端口、没有 mDNSResponder 行;控制组:去掉 N3(= 老勘破 §5 那两行)或加一条减法 deny ⇒ 红。

AC3 不误伤 —— 出货形态六格(docs/verification/2026-09-10-req137-1337-packaged-egress/,47 pass / 0 fail / 3 obs,rc 0)

签名包 com.tide.alphacode 0.1.12(flags=0x10000(runtime),TeamIdentifier=RQX6X6A635,lipo = x86_64 arm64,codesign --verify --deep --strict exit 0;asar 里 network egress policy proxy listening ×1、ALPHA_AC1334_NETWORK ×0)。runner 不设任何代理 env、不起外部代理

读数
1 冷启动 health 200;network egress policy proxy listening on 127.0.0.1:55088process fence planned: … egressProxyPort=55088, profile=2531Bprocess fence applied … profile=2531B
1 sidecar env(shell 工具里实读) HTTPS_PROXY=HTTP_PROXY=ALL_PROXY=http://127.0.0.1:55088,NO_PROXY=no_proxy=127.0.0.1,localhost,::1
2 连接器(MCP stdio) connected;界内落盘、界外 0
2 provider 安装(两处目录实读) 26 包 / 62 636 KB × 2 处(<XDG_CONFIG_HOME>/opencode/node_modules<userDataPath>/alpha-engine-config/node_modules),~/.npm/_cacache 在;registry.npmjs.org:443 经闸门 allow 15 / deny 0;引擎日志零 NpmInstallFailedError
3 开终端 通;终端里 $HTTPS_PROXY=http://127.0.0.1:55088;界外 zsh: operation not permitted
4 shell 工具 通;界内落盘、界外 0
5 写配置 + ext 装载 通(alpha.jsonc / alpha-engine-config/ / alpha_register)
6 三工作区并集(SIGKILL → self-heal) workspaces=3,profile=2715B,第二代 egressProxyPort=55088 同一端口,proxy listening 全程 1 条;重算后第一条 shell 命令 100 ms(#1334 deny 臂 120 s)

误伤语料 12/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(预期,解析在代理侧)。

逃逸语料(出货形态,每条带 AC1337-STARTED)实际输出:

curl https://example.com                 → curl: (56) CONNECT tunnel failed, response 403      main.log: deny/unregistered example.com:443 ×1
curl --noproxy '*' https://1.1.1.1/      → curl: (7) Failed to connect to 1.1.1.1 port 443 after 0 ms
curl --noproxy '*' https://github.com/   → curl: (6) Could not resolve host: github.com
node net.connect(443,"1.1.1.1")          → TCP=EPERM
node dgram.send(…,53,"1.1.1.1")          → UDP=EPERM
node net.connect({host:"::1",port:N})    → V6=EPERM
远程 MCP http(s)://ac1337-probe.invalid → 引擎进程内 fetch: "Proxy response (403) !== 200 when HTTP Tunneling"   main.log: deny/unregistered ac1337-probe.invalid:80 ×2 / :443 ×2

最后一行是 #1334 Q4 那条探针的升级:引擎自己的出网经闸门,而且被拒的原因是「不在注册表」(403),不再是「解析不了」。

AC4 fail-closed 且要快 —— network-egress-fence.test.ts A3

代理 close() 之后同一 profile 再跑一遍,实际输出:

[ac1337 A3 fenced/down] proxyFetch={"ok":false,"code":"ECONNREFUSED","message":"connect ECONNREFUSED 127.0.0.1:53782","ms":6} proxyConnect={"ok":false,"code":"ECONNREFUSED","message":"connect ECONNREFUSED 127.0.0.1:53782","ms":1} directTcp={"ok":false,"code":"EPERM","message":"connect EPERM 127.0.0.1:53781 - Local (0.0.0.0:0)","ms":0}

断言:errno 是 ECONNREFUSED(不是 ETIMEDOUT / 探针超时)、消息点名代理地址 127.0.0.1:<port>、绕代理直连仍 EPERM(不回退直连)、监听不受影响。「快」由 errno 结构上给出(loopback 上没人听的端口是内核即时 RST),耗时只打印不断言(#1300 棘轮:不新增「机器有多闲」的判据)。

接线侧(process-fence-wiring.test.ts +3):代理起不来 ⇒ network egress policy proxy failed to start — sidecar fork refused: …EADDRINUSE,fork 0 次、计划器 0 次;缺省提供者真起监听、明文 GET 405 alpha egress policy、两次 fork 同一端口;fork 的 env 八个变量整份改写,用户导出的 HTTPS_PROXY=http://127.0.0.1:7897 / NO_PROXY=github.com,.internal 不存活,而 main 的 process.env 原值不变。

归因(硬要求四,process-fence-profile.test.ts / process-fence-plan.test.ts):unbound variable: host 那种失败 ⇒ 只试编 1 次、0 dropped 即抛:… 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;字节墙仍丢尾至 (1 workspace, 3 dropped, 4 attempts)

AC5 覆盖面如实声明 —— network-egress-disclosure.test.ts(4 条,全平台)

三个落点(终端悬停卡 zh / en + 勘破文档锚点段落)都要逐字含独立字面量(已登记地址 / 其它地址被拒 / 应用自身联网不在其内 / Electron main 与 renderer 不在覆盖内),且不许出现过度声明。反向实际输出:

  • 把声明改成「所有联网都被这道沙箱限制。」/ "All network traffic is restricted by this sandbox." / 「全部出网都被这道围栏限制。」⇒ 三处各判红:overclaims a larger protection surface than exists: "所有联网都被" + missing: "…不在这道沙箱之内"
  • 只删掉排除句 ⇒ 红并点名缺 不在这道沙箱之内 / is outside this sandbox;锚点整段删除 ⇒ 红。
  • 呈现层:terminal-rail.cases.ts 加一条 toContain("Code Puppy 应用自身的联网(模型目录、登录、检查更新)不在这道沙箱之内")(9/9)。

本地门(worktree .worktrees/ac-1337,bootstrap 过;electron dist 软链自主 checkout 只读)

读数
north-star ✓ zero upstream package edits,rc 0
typecheck bun run typecheck(tsgo -b)rc 0
unit(base) bun test src @ origin/alpha@59b26f136,同一棵树改动前:4977 pass / 0 fail,357 files,291.55 s,rc 0
unit(head) bun test src @ head(同一棵树):4995 pass / 0 fail,359 files,279.06 s,rc 0 —— 相对 base +18 pass / +2 files = 本票的 18 条(profile +4 · plan +2 · wiring +3 · sidecar-env +1 · fence 4 · disclosure 4),新增红 0
闸门文件点名 bash scripts/assert-gate-files.sh:✓ 202 个闸门文件全部在位且真的跑过(条数与登记精确一致),GATE_RC 0(第一轮把 plan 测试登记成 11 被抓:实际 10 —— 改写 1 + 新增 2;改回 10 后重跑全量)
docs check-doc-links.py 四个改动的 md:✓ 15 relative link(s) resolve across 4 file(s),rc 0(控制:一个坏链接的探针文件 rc 1)
模块体积棘轮 packages/ui-mac/src/main 实测 55,726 = 基线(从 55,592 抬 +134,全部是本票的,TSV 那行写明;#1336 已把前几张票遗留的 +247 抬进实测值)。脚本 rc 2:两处「改动文件超 800 行」告警是 renderer/i18n/{zh,en}.ts(1717 / 1755 行,单文件字典是既有形态,本票各加一句,不在本票拆)
write-sites / wall-clock / gate-file-registry 通(新增写盘调用点 0,墙钟上界断言 0)

主动没做的

  • 不改代理本体、注册表匹配语义、可写集文件规则、并集裁剪规则(票面边界)。
  • 非 darwin 不装策略层:没有围栏的平台既不起代理也不改 sidecar env(如实:不假装有一半)。
  • 127.0.0.1:11434(ollama)在注册表里但围栏不放行 localhost:11434,且 NO_PROXY127.0.0.1 ⇒ 围栏下不可达。本票没有 ollama 负载,未测;是注册表与 profile 之间一条已知张力,归 #1073 裁(加一行 N 规则或删表行),没自作主张。
  • 出货形态「杀掉代理」到不了(代理住在 main,main 死则 sidecar 一起死);AC4 端到端在 Electron-node 测试里做。
  • renderer.home.catalog_ready / catalog_liveness.confirmed 时序没取到(main.log 里没有那两个键);替代读数「重算后首条 shell 命令 100 / 98 ms」只两轮,按《时序门需 ≥3 轮采样》不构成结论。
  • 没有发过一次真的模型请求;models.opencode.ai 那条 fetch 仍没发生(与 #1334 同)。
  • 设计稿 docs/design/2026-09-10-req159-sandbox-disclosure/design.md §3.1 文案表(悬停层「三句」)没有回填第四句 —— 那是 design-loop 的事,请主 session 裁。
  • #1334fixture/profile-byte-budget.ts 现在调 renderProcessFenceProfile 会缺 egressProxyPort 抛错(它是历史取证夹具,里面手工追加网络行的两条臂已被生产渲染器替代),没改。
  • $? 经引擎 shell 工具读回恒 0(两轮实测)没追根;harness 改判 curl 自己的错误文本。

🤖 Generated with Claude Code

https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN

jinjunnn and others added 2 commits September 10, 2026 06:23
REQ-137 的强制半场。profile 尾部追加 #1334 Q1.2 那四行(deny network* + loopback bind/inbound
+ 只放行 localhost:<代理端口> 出网;只写加法、DNS 不放行);策略代理在 Electron main 进程内
fork 之前起、跨 respawn 复用、起不来即拒 fork;sidecar 的八个代理变量由 main 整份改写指向它
(用户自己的代理 / NO_PROXY 不存活,main 的 process.env 不动)。trimUntilCompiles 的归因:非字节墙
的编译失败一个工作区都不丢即抛。注册表按 #1073 裁决二加 release-assets.githubusercontent.com:443。

判据:network-egress-fence.test.ts(Electron 的 node + 真 .node + 真 seatbelt + 真代理:唯一那扇门通、
逃逸语料逐条 EPERM、代理关掉后 ECONNREFUSED 且不回退直连、bare 臂对同一判据必红)、
process-fence-{profile,plan,wiring}.test.ts、sidecar-env.test.ts、network-egress-disclosure.test.ts
(AC5:终端悬停卡 zh/en + 勘破文档锚点段落的覆盖面声明,过度声明必红)。出货形态六格 + 误伤语料 +
逃逸语料在 docs/verification/2026-09-10-req137-1337-packaged-egress/(47 pass / 0 fail / 3 obs,
两处 provider 目录各 26 包)。

Fixes #1337
Refs #1073

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN
`#1073` owner 2026-09-10 裁决一。注册表里 `127.0.0.1:11434`(ollama 一类本机模型)是一行做不到的话:
围栏只放行 `(allow network-outbound (remote ip "localhost:<代理端口>"))`,别的 loopback 端口一律 EPERM;
而 NO_PROXY 含 loopback ⇒ 这类目的地本来就不经代理。两边一夹,注册表对它永远不生效。

主 session 实测(三臂):围栏下连 127.0.0.1:11434 = EPERM,连放行的代理端口 = ECONNREFUSED
(即围栏放行),无围栏对照 = CONNECTED。**BYOK 指向 loopback 的 baseURL 撞的是同一堵墙**,
且本就属于「动态」类别、静态表里登记不了 —— 删这行不改变 BYOK 的处境。

- 注册表删行 + 写明理由(含实测读数与「要支持本机目的地请先设计,不要往表里加行」)。
- 新判据:表内不得出现任何 loopback 目的地。反向臂实跑 —— 加回一行 ⇒ 三条红,新判据带完整理由。
- 连带修正三处把该行当期望值钉着的测试(初值清单、授权语义正样本、代理默认 authorize 正样本):
  正样本换成 github.com:443,负样本换成同 host 的 22 端口。**每一处都是正确的红,改的是测试不是判据。**
- gate-files.tsv:registry 条数 4→5 并补新判据说明;proxy 那行描述里已删的地址改成实际用的。

门:typecheck rc=0 · ui-mac 全量 4996 pass / 0 fail / 359 files · north-star rc=0 ·
assert-gate-files `✓ 202 个闸门文件…精确一致` rc=0。

Refs #1073

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN
@jinjunnn
jinjunnn merged commit 4e50c8c into alpha Sep 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ-137][CODE] 把路封死:围栏只放行看门人那扇门,并如实说明这道防护罩不到哪

1 participant