[ac#1322] 沙箱下的两处如实告知:终端脚条「沙箱开启」+ 工作区「只读」胶囊,探针由被围栏的 sidecar 真写一次 - #1330
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):还原后 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.ts的useWorkspaceWritable(dir),两个宿主各挂一次 —— 会话页live.current().identity.directory(deep link / draft 晋升都落这里),新对话页与首页 chip 的activeWs(chip 选目录 / 侧栏resolveDraftTarget都落这里)。三个触发点不各写一遍。只在沙箱真装上时才探;答案按「引擎代 + 目录」缓存(respawn 换代重问,可写集每次 fork 重取并集)。判据(两臂):
test-component/session-workspace.cases.ts2 条(集合内无胶囊;集合外胶囊紧跟状态胶囊、role=status、悬停/点击开弹层、重启动作真调 relaunch、Esc 关、切回可写即消失);test-component/new-session-workspace.cases.ts3 条(新对话页:/ws/a答 writable 无标、选到/ws/b答 denied 尾标当场出现且在箭头之前、切回同代不重复探;unknown / 探针出错 / 桥缺席 都无标;沙箱没装一次都不探;首页同一宿主);workspace-writable-core.test.ts7 条;sandbox-state.test.ts4 条。反向用例实跑(顶栏胶囊与 chip 尾标的出现条件都改成
false):AC3 探针必须先答过「可写」才算数
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.tsB2 实测)/unknown(ENOENT、ENOTDIR、EACCES=文件系统权限位而非围栏、超时、引擎不在)。链路:server.tslistener.probeWrite(请求簿按 id 对号,5 s 超时 / 子进程退出 ⇒ unknown)→ipc.tsworkspace-write-probe→ preloadworkspaceWriteProbe。写入点已登记scripts/process-fence-write-sites.tsv(根 W1;process-fence-write-sites.test.ts绿,78 条登记 / 94 个调用点)。workspace-write-probe-fence.test.ts(真 .node 现编 + 真 seatbelt + 真子进程)断言out.pid !== process.pid、out.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" })红)。fs.writeFile探的假探针):bare 臂实跑inside writable / outside writable⇒rejected: 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.tsstart()第一句就是installProcessFence,apply 失败 ⇒ error IPC + exit(1),永远发不出 ready —— 接线锚process-fence-wiring.test.ts)」时返回fence:"applied";settleBootHealth/armRespawnGenerationTerminal把它挂到引擎在线的终态上(ready/injection-failed,failed不带),经既有sidecar-generationIPC 到 renderer;alpha-ui/sandbox-state.ts订阅既有 runtime-recovery 通道投影成sandboxApplied(),经 shell 的sandboxprop 递进终端面板(面板 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 无冲突,自动合并)。bun run --cwd packages/ui-mac typecheck,rebase 后重跑):$ tsgo -b→ rc=0,error TS0 条。bash scripts/north-star-guard.sh,rebase 后重跑):rc=0,✓ zero upstream package edits(… baseline origin/alpha …)。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 的目录在本分支树上还不在,链接会红)。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.ts4→5;gate-file-registry.test.ts22/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 修复后的树(HEAD56ba3c2e1)上整包:4943 pass / 0 fail,Ran 4943 tests across 354 files. [289.69s],rc=0。与 base fail-set 的差
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/dist与path.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(WorkspaceReadonlyPill)·alpha-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读 bytesNUL 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 的探针文件自证会命中。主动没做的
run。sidecar.ts顶层registerHooks/getParentPort结构上 import 不了,探针在 sidecar 进程里的执行由 fence 测试用 bun 子进程 + 生产applyProcessFence复刻;sidecar.ts那一跳只有parseCommand/消息循环的源码改动,没有源码锚(刻意不加 ANCHOR)。classifyWriteProbeError一行 + 一条测试。🤖 Generated with Claude Code
https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN