Skip to content

[ac#1322] 沙箱下的两处如实告知:终端脚条「沙箱开启」+ 工作区「只读」胶囊,探针由被围栏的 sidecar 真写一次 - #1330

Merged
jinjunnn merged 1 commit into
alphafrom
feat/1322-sandbox-disclosure
Sep 10, 2026
Merged

[ac#1322] 沙箱下的两处如实告知:终端脚条「沙箱开启」+ 工作区「只读」胶囊,探针由被围栏的 sidecar 真写一次#1330
jinjunnn merged 1 commit into
alphafrom
feat/1322-sandbox-disclosure

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #1322
Refs #1286

围栏装上之后用户看不出原因的两件事(终端配置部分静默失效;启动后才打开的项目写不进去),按 owner 2026-09-09 裁决不放宽、不改行为,改为如实告知。实现照已批设计稿 docs/design/2026-09-10-req159-sandbox-disclosure/(PR #1329,帧内「重新启动」按钮保留)。不碰围栏本体、可写集、sidecar.ts 的 fence 段、PTY 起登录 shell 的行为;不碰 docs/design/

三条 AC 各自的判据与反向用例(实跑读数)

AC1 终端面静态告知 + 判据守位

  • 呈现:terminal-rail-panel.tsx 脚条第三项(env 之后、size 之前,data-alpha-terminal-foot-sandbox;窄于 360px 用容器查询只留「沙箱开启」,整句进 title + 非模态卡)+ 空态一行(data-alpha-terminal-empty-sandbox,正文之下、按钮之上)。

  • 判据:test-component/terminal-rail.cases.ts(宿主 terminal-rail.test.ts)3 条 —— 脚条项位置(按 foot.children 顺序判 env < sandbox < size)、空态行位置、反向臂(信号关 / prop 缺席 ⇒ 两个钩子都不在;信号翻真翻假实时跟随)。文案锚点是设计稿的独立字面量,不从 dict 读回。

  • 反向用例实跑(把脚条项的出现条件改成 <Show when={false}>,跑 terminal-rail.test.ts):

    (fail) REQ-159 #1322 sandbox disclosure on the terminal face (AC1) > sandbox on: the foot carries a third item after env and before size; …
          expect(item).not.toBeNull()   error: expect(received).not.toBeNull()
    (fail) … > reverse arm: sandbox off — and the prop absent … the item follows the signal live
     12 pass / 2 fail  →  宿主 (fail) REQ-125 C3-term real Solid mount > component cases …   8 pass / 1 fail
    

    还原后 9 pass / 0 fail。

AC2 工作区只读告知(动态)

  • 呈现:session-workspace-shell.tsx 顶栏状态胶囊之后一枚「只读」胶囊(data-alpha-workspace-readonly,role=status,--a-warning 系,与状态胶囊同款 24px)+ 弹层(标题 / 正文 / 脚注 /「重新启动 Code Puppy」→ 既有 window.api.relaunch;悬停与点击都能开,Esc 关);workspace-chip.tsx 目录名与折叠箭头之间的尾标(data-alpha-workspace-chip-readonly),首页与新对话页共用。

  • 四态:探针未答 / 可写 / 未知 ⇒ 无标记;只有引擎明确回报 denied ⇒ 标记(workspace-writable-core.ts,纯状态机)。

  • 单一咽喉:workspace-writable.tsuseWorkspaceWritable(dir),两个宿主各挂一次 —— 会话页 live.current().identity.directory(deep link / draft 晋升都落这里),新对话页与首页 chip 的 activeWs(chip 选目录 / 侧栏 resolveDraftTarget 都落这里)。三个触发点不各写一遍。只在沙箱真装上时才探;答案按「引擎代 + 目录」缓存(respawn 换代重问,可写集每次 fork 重取并集)。

  • 判据(两臂):test-component/session-workspace.cases.ts 2 条(集合内无胶囊;集合外胶囊紧跟状态胶囊、role=status、悬停/点击开弹层、重启动作真调 relaunch、Esc 关、切回可写即消失);test-component/new-session-workspace.cases.ts 3 条(新对话页:/ws/a 答 writable 无标、选到 /ws/b 答 denied 尾标当场出现且在箭头之前、切回同代不重复探;unknown / 探针出错 / 桥缺席 都无标;沙箱没装一次都不探;首页同一宿主);workspace-writable-core.test.ts 7 条;sandbox-state.test.ts 4 条。

  • 反向用例实跑(顶栏胶囊与 chip 尾标的出现条件都改成 false):

    (fail) REQ-159 #1322 workspace read-only tag on the chip (AC2) > new-session page: the tag appears only after the fenced engine answers denied …
    (fail) REQ-159 #1322 workspace read-only tag on the chip (AC2) > home page: the chip is the same host …
    (fail) REQ-159 #1322 workspace read-only disclosure on the topbar (AC2) > outside the writable set: a 只读 pill sits right after the status pill …
     → 两个宿主各 1 fail(new-session-workspace.component.test.ts 0/1;alpha-session-workspace.test.ts 11 pass / 1 fail)
    

AC3 探针必须先答过「可写」才算数

  • 探针在 sidecar 进程里执行:main/workspace-write-probe.ts(合同 + 执行),sidecar.ts 消息循环收到 {type:"write-probe"} 即在目标目录以 wx 写一个 .alpha-write-probe-<戳> 再删,回 {type:"write-probe-result"}。三种回答:writable / denied(只有 EPERM —— seatbelt 的拒绝码,process-fence-apply.test.ts B2 实测)/ unknown(ENOENT、ENOTDIR、EACCES=文件系统权限位而非围栏、超时、引擎不在)。链路:server.ts listener.probeWrite(请求簿按 id 对号,5 s 超时 / 子进程退出 ⇒ unknown)→ ipc.ts workspace-write-probe → preload workspaceWriteProbe。写入点已登记 scripts/process-fence-write-sites.tsv(根 W1;process-fence-write-sites.test.ts 绿,78 条登记 / 94 个调用点)。
  • 探针落在 sidecar 的证据:workspace-write-probe-fence.test.ts(真 .node 现编 + 真 seatbelt + 真子进程)断言 out.pid !== process.pidout.apply.buildId === 本次编译的 buildId;围栏臂读数:inside {outcome:"writable"},outside {outcome:"denied", detail:"write EPERM: …"},missing unknown / write ENOENT,两边目录零残留。
  • 判据 judgeWriteProbeCalibration(workspace-write-probe-judge.ts,测试旁的纯函数):先在可写工作区答过 writable、再在集合外答 denied 才算数。
  • 两个替身被判据当场拒掉(实跑输出原文):
    • 恒答「不可写」:workspace-write-probe.test.ts恒答「不可写」的替身被当场拒掉 —— rejected: answers "denied" for a writable workspace — it would mark every project read-only(pass = 判据拒了它)。变异实跑:把 runWorkspaceWriteProbe 的成功路径改成恒 denied,跑 fence 文件 ⇒ 0 pass / 2 fail(围栏臂 expect(out.inside).toEqual({ outcome: "writable" }) 红)。
    • 恒答「可写」(= 在 main 里 fs.writeFile 探的假探针):bare 臂实跑 inside writable / outside writablerejected: answers "writable" outside the writable set — it never reports a read-only workspace (that is what an unfenced / main-process probe says)。变异实跑:把 classifyWriteProbeError 改成恒 "writable",跑两份探针测试 ⇒ 11 pass / 5 fail,其中围栏臂 Expected: "denied" Received: "writable"(workspace-write-probe-fence.test.ts:87)。

沙箱信号从哪来(AC1 的出现条件)

引擎自报,renderer 不猜平台:spawnLocalServer 只在「fence 计划进了 start 命令(darwin 上没有计划走不到这里)+ sidecar 发了 ready(sidecar.ts start() 第一句就是 installProcessFence,apply 失败 ⇒ error IPC + exit(1),永远发不出 ready —— 接线锚 process-fence-wiring.test.ts)」时返回 fence:"applied";settleBootHealth / armRespawnGenerationTerminal 把它挂到引擎在线的终态上(ready / injection-failed,failed 不带),经既有 sidecar-generation IPC 到 renderer;alpha-ui/sandbox-state.ts 订阅既有 runtime-recovery 通道投影成 sandboxApplied(),经 shell 的 sandbox prop 递进终端面板(面板 I1 棘轮禁 window.api)。非 darwin ⇒ 一路缺席 ⇒ 不出现。判据:process-fence-wiring.test.ts(result.fence === "applied" + 探针往返)、sidecar-generation.test.ts +2、sidecar-lifecycle.test.ts +1。

本地门(真实输出)

基线 = origin/alpha @ 5daf6bdfa(#1328 已合,本分支已 rebase 到它之上;基线里 docs/architecture/2026-09-09-req159-process-fence.md §7 首条与本 PR 的 §7 尾三条 + §8 无冲突,自动合并)。

  • typecheck(bun run --cwd packages/ui-mac typecheck,rebase 后重跑):$ tsgo -b → rc=0,error TS 0 条。
  • north-star(bash scripts/north-star-guard.sh,rebase 后重跑):rc=0,✓ zero upstream package edits(… baseline origin/alpha …)
  • docs gate(python3 scripts/check-doc-links.py docs/architecture/2026-09-09-req159-process-fence.md,rebase 后重跑):✓ 3 relative link(s) resolve across 1 file(s),rc=0。设计稿路径刻意不用相对链接(PR [ac#1322 设计稿] 沙箱下的两处如实告知 —— owner 2026-09-10 已批,增量帧并入活稿并铸锚 #1329 的目录在本分支树上还不在,链接会红)。
  • write-sites 登记簿(bun packages/ui-mac/scripts/process-fence-write-sites.ts):✓ 78 条登记与扫描一致(94 个调用点)
  • 闸门登记簿:gate-files.tsv 新登 workspace-write-probe.test.ts(14,委派 fence 文件)与 workspace-write-probe-fence.test.ts(2,[平台:darwin]),process-fence-wiring.test.ts 4→5;gate-file-registry.test.ts 22/22 绿。
  • 定向:terminal-rail.test.ts / alpha-session-workspace.test.ts / new-session-workspace.component.test.ts(含三个子进程 cases 全绿)+ sidecar-generation / sidecar-lifecycle / process-fence-wiring / process-fence-write-sites / ipc-channel-binding-census / alpha-config-injection / gate-file-registry / locale-regression / brand-residue-scan / sidecar-ready-message ⇒ 120 pass / 0 fail(11 文件);workspace-write-probe* + workspace-writable-core + sandbox-state ⇒ 全绿。
  • 全量 bun test src(ui-mac):rebase + NUL 修复后的树(HEAD 56ba3c2e1)上整包:4943 pass / 0 fail,Ran 4943 tests across 354 files. [289.69s],rc=0。

与 base fail-set 的差

  • 新增红:0
  • 第一轮(rebase 前、bootstrap 出来的 worktree 原样)整包是 4932 pass / 1 fail,唯一的红是 process-fence-apply.test.ts 的 beforeAll:ENOENT … electron/path.txt —— worktree 环境,与本 PR 无关:bun install 不跑 electron 的 postinstall 下载,.worktrees/ac-1322 / ac-1323 / ac-1248 三棵树里都没有 electron/distpath.txt,只有主 checkout 有。本轮把主 checkout 的 electron/dist 软链进本 worktree 的 electron 包(只读用它的二进制,不往主 checkout 写任何东西)后,该文件 11 条全绿,整包 0 红。这条值得进《本机验证陷阱》:bootstrap 出来的树上 darwin-only 的 Electron 内嵌 node 判据会以 ENOENT 假红。

文案与设计稿的一处差别(有意)

设计稿文案里的产品名写的是「Alpha」;仓内 brand-residue-scan.test.ts 对 en/zh/zht dict 级禁独立词 Alpha(REQ-139),且既有条目一律写「Code Puppy」(alpha.brand.product)。所以「Alpha 自己的目录 / Alpha 启动时 / 重新启动 Alpha」在 dict 里落成「Code Puppy …」,其余逐字。

台账回填

本 PR 不动 docs/design/ 下任何文件。components.md 两行的「落地」与「代码入口」由主 session 在本 PR 合入后回填(代码入口:终端面 = session-rail/terminal/terminal-rail-panel.tsx + alpha-ui/sandbox-state.ts;只读 = session-workspace/session-workspace-shell.tsx(WorkspaceReadonlyPillalpha-ui/workspace-chip.tsx · alpha-ui/workspace-writable.ts · main/workspace-write-probe.ts)。

复核修掉的一处缺陷(复核方发现)

workspace-writable-core.ts 的复合键分隔符第一版是裸 NUL 字节落进源码(git 判 binary,PR 里不可读;仓内治理明写「NUL 让 grep 假阴」)。已改成 TS 转义 \u0000(运行时字节相同)。自证:python3 读 bytes NUL count before: 1 → after: 0;git show HEAD:<file>b'\x00' in blob = False;git diff --stat origin/alpha...HEAD 该文件从 Bin 变回 81 insertions(+)。顺带扫了本分支触及的全部 41 个文件(bytes < 0x20 且非 tab/lf/cr):0 个含裸控制字符;扫描器先用一个已知 NUL 的探针文件自证会命中。

主动没做的

  • 没有在真 app 里跑过一次 / 没截图:判据全部是 happy-dom 真挂载 + 真围栏子进程;悬停层 / 容器查询窄宽 / 弹层箭头这些视觉只按帧写了 CSS,没有 L2 目测。主 session 决定要不要派一次 run
  • 没有在出货形态(打包 sidecar)上跑过探针 IPC:sidecar.ts 顶层 registerHooks/getParentPort 结构上 import 不了,探针在 sidecar 进程里的执行由 fence 测试用 bun 子进程 + 生产 applyProcessFence 复刻;sidecar.ts 那一跳只有 parseCommand/消息循环的源码改动,没有源码锚(刻意不加 ANCHOR)。
  • EACCES 归 unknown 而不是 denied(只认 EPERM):披露文案把原因归给沙箱并承诺重启即可,对文件系统权限位不可写的目录那是假话。若 owner 更想「凡写失败都标」,改 classifyWriteProbeError 一行 + 一条测试。
  • 台账回填(见上,归主 session)。
  • 没有给弹层做「点击外部关闭」(设计只写了悬停/点击开;Esc 与失焦关已做)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN

AC1 终端面:脚条第三项(env 之后、size 之前)+ 空态一行 + 悬停层,只在引擎自报 fence:applied 时出现
AC2 工作区:顶栏只读胶囊 + 弹层(重新启动走既有 relaunch)+ chip 尾标;四态里只有 denied 标记
AC3 探针:sidecar 进程里写-删(EPERM ⇒ denied,其它 unknown);判据拒掉恒答不可写 / 恒答可写(= main 里探)两个替身

Refs #1322

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN
@jinjunnn
jinjunnn merged commit 95880b1 into alpha Sep 10, 2026
5 of 6 checks passed
jinjunnn added a commit that referenced this pull request Sep 10, 2026
`docs/design/README.md` 说得明白:「A component whose ledger row still reads 设计中 after its
issue closed is the drift this layer exists to surface.」这次是那层自己漏了。

- 沙箱告知两行(`#term-sandbox` / `#wtop-readonly`):`ac#1322` 已随 PR #1330 合入
  (`95880b18f`,2026-09-10),落地日与代码入口按实读回填,状态转「已实现」。
- **右栏宽度规则(`ac#1161`)**:该票 2026-08-29 就由 PR #1169(`3665829bd`)关闭,代码里
  固定上限 560 已彻底移除(`rail-width.ts` 现为 320 / 400 / 480 三常量 + 按实测工作区宽收敛),
  而台账那行仍写着「设计中 · 固定上限 560,待改」—— **陈旧 12 天**。一并订正。

回填前对五份台账 41 行做了一次全量对照(行状态 vs 票的真实状态):未结行 6 处,其中票已
全部关闭的漂移 1 处 = 上面这条;回填后重扫,漂移 0 处。剩余 5 处未结行的票确实仍开着
(ac#245 / ac#246 / ac#1130)或尚未立票,不是漂移。

Refs #1322


Claude-Session: https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN

Co-authored-by: jinjunnn <slmbaovanetti99@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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-159][CODE] 沙箱下的两处如实告知:终端配置可能部分不生效、这个项目现在写不进去

1 participant