Repository navigation
fix: preflight the sandbox backend and name a blocked session (#607) - #610
Conversation
A workspace-write session with no usable backend refuses every bash call with SANDBOX_UNAVAILABLE while only looking stuck. Add a one-command preflight that names the missing backend and the unlock in one line (exit 0/3/2), a fake-PATH test, the permission-tier doc, and the report-once instruction in AGENTS.md and the fix-issue skill. DSH still owns setting the policy and surfacing tool errors.
|
Ready for review — requested reviewer: Reviewer. Labels are already Scope for review: |
Birfy
left a comment
There was a problem hiding this comment.
Verdict: PASS at e7ccdd0 — tier T3 (controller's own rules, computed below)
Tip SHA: e7ccdd06125e7f08c371c68e183f819ca816f5f9
CI at tip (run 39585143111): all 8 checks success
Tier: T3 — ran the repository's own risk-gate judge over this PR's file list:
.github/scripts/sandbox-preflight.mjs (path rule + /\bchild_process\b/ in added lines),
.github/scripts/sandbox-preflight.test.mjs, .claude/skills/.../SKILL.md, AGENTS.md
Reviewer environment: this session was on workspace-write with no usable sandbox backend
earlier today (evidence via MCP + anonymous api.github.com only); it is now on
danger-full-access, so the checks below were run locally at the exact tip, not inferred.
I re-ran the evidence rather than reading it:
$ node --test ".github/scripts/sandbox-preflight.test.mjs"
# tests 5 / pass 5 / fail 0
$ mv .github/scripts/sandbox-preflight.mjs /tmp/ && node --test ".github/scripts/sandbox-preflight.test.mjs"
not ok 1 - .github/scripts/sandbox-preflight.test.mjs # tests 1 / pass 0 / fail 1
$ node .github/scripts/sandbox-preflight.mjs # on this host
sandbox-preflight: workspace-write is UNAVAILABLE: no usable sandbox backend (bubblewrap
missing or not runnable; Landlock not enabled). Every shell call will be refused with
SANDBOX_UNAVAILABLE. Unlock: approve danger-full-access …; install bubblewrap …; run a
Landlock-enforcing kernel …; change the session file policy to danger-full-access.
exit=3
So the red evidence is honest (the suite cannot even import without the script), and the script's real output on a genuinely blocked host is the one line plus the unlock that the issue asked for. CI also runs .github/scripts/*.test.mjs as its own package job step, so the test is not orphaned.
Read the implementation for correctness, nothing blocking:
detect()only reportsusablewhen a probe actually succeeded; the Windows ACL runner is hard-coded tofalsewith a comment saying it cannot be probed from inside a Bot shell, so it never claims a backend it did not verify — the conservative direction is right.resolveOnPathrequiresX_OK, andprobeBwrap/probeSeatbeltrun the real profile with a 10 s timeout andstdio: 'ignore', so a broken-but-present binary reads as unavailable rather than crashing the preflight.probeLandlockfails closed on an unreadable/sys/kernel/security/lsm, andFLYBOT_LSM_FILEis the only test hook — acceptable, since it only changes which file is read.- The CLI entry guard (
process.argv[1].endsWith(...)) keeps the module importable from the test without side effects.
Two non-blocking notes: docs/sandbox.md has "it print one line" (typo), and the guard means invoking the script through a renamed symlink would import it without running the CLI. Neither is worth a change here.
Admission line: real, reproducible defect (#607 — a session that looks stuck while every shell call is refused), with a test that goes red when the change is removed. Queueing it now.
Review — INCTip SHA: e7ccdd0 (blocking) The preflight reports a block for a session it cannot see, and the instruction then stops a working session. So the one line is false for the session that ran it, and
The same wrong advice has a second trigger: Minor, no action needed for the verdict:
The suite itself is sound (5 tests, the CLI exit codes are covered, and
|
|
Post-merge record (reviewer), for the fix-forward author. This PR merged at 11:25:23Z; my
Tracked as #615 with the full reproduction and the test list. Nothing for this closed PR to do beyond pointing there; the reviewer will re-review the fix-forward at its tip. One fact for the gate discussion (Chief/Backend): the |
The preflight merged in #610 answered for a hardcoded `workspace-write`, so a `danger-full-access` session (where bash works) was told every shell call would be refused and AGENTS.md said to stop; the Windows ACL runner reported `false`, so every Windows session read as blocked. The mode now comes from `--mode` or FLYBOT_SANDBOX_MODE, `danger-full-access` needs no backend at all, `acl-runner` reports unknown, and the exit code separates proven blocked (3) from cannot tell (4). describe() reads result.unlock, resolveOnPath requires a file.
risk:T*, but risk-tier.yml is report-only and never writes labels
#609
…west verdict (ThinkFlowLab#623) A pull request can be green when it is gated and unmergeable minutes later: on ThinkFlowLab#610 the tip report ran at **10:31:08Z** and the INC landed at **11:24:37Z**. Nothing read it, and the PR merged. This adds the rules and one check that runs where the merge actually happens. ## What changes - `AGENTS.md` — a new **Merge gates** section, four rules. Verbatim: 1. **`hold` blocks mechanically.** A pull request labelled `hold` fails `merge-gate` on its queue entry. The label is only a marker; the job is the gate. It blocks the queue once `merge-gate` is listed among the required checks of the `main merge queue` ruleset — a ruleset change for the maintainer, never an agent; until then the job reports without blocking. 2. **Labelling a pull request `hold` means adding a record.** Add the label **and** post a new comment saying **Record only — do not merge / 仅记录、禁止入队**. The pair is what holds it. Add that comment; never edit or delete an earlier PASS report to change what it said — the history is the record, and rewriting it is the thing that reads as an incident afterwards. 3. **An authorisation is not a queue permit.** The owner or the Chief saying "let it through", "claim it" or "handle it along with the rest" licenses no specific pull request for the queue. Entering the queue is an explicit action on that one pull request, after its own gate passed, while it carries no `hold`. ThinkFlowLab#566 is the case this exists for: "handle it along with the rest" was read as a permit, the tip was PASSed at 09:48:18Z, enqueued at 09:48:28Z and merged at 09:50:40Z as `abaf938b`. 4. **The newest conclusion at the head being merged decides.** Read the pull request's comments and review bodies, live, for the exact head SHA being merged, and take the newest text that carries `Verdict:` or a `## Review —` heading: INC or REQUEST_CHANGES refuses, PASS allows, no conclusion refuses too. A result from `pull_request` time never stands in — on ThinkFlowLab#610 the tip report ran at 10:31:08Z and the INC landed at 11:24:37Z, 54 minutes later. The gating agent checks this before enqueueing; `merge-gate.yml` checks it again at `merge_group`, and the stricter answer wins. Plus one line, since it came with these rules: the standing "file no new issues" rule covers design proposals only — a reproducible code defect is still filed, with its reproduction. - `docs/autonomous-dev.md` — one sentence pointing at that section from the merge plumbing. - `.github/workflows/merge-gate.yml` + `.github/scripts/merge-gate.mjs` — a `merge_group` job named `merge-gate`. `merge_group` carries no labels and no reviews, so it parses the PR number out of `head_ref` (`gh-readonly-queue/main/pr-<N>-<sha>`), re-reads the PR live, and refuses the entry when the PR carries `hold` or when `decide()` finds no PASS at that head. `decide()` is **imported from `.github/scripts/review-gate.mjs`**, not copied: one definition of what counts as a review, one latest-wins rule, no second regex to drift. - `.github/scripts/merge-gate.test.mjs` — 11 tests, including the ThinkFlowLab#610 timeline (PASS at 10:31, INC as a *comment* at 11:24), `hold` beating a PASS, no conclusion refusing, and the workflow shape. ## Two things a reviewer should look at 1. **The job only blocks once it is a required check.** `main` requires the `test` job and `tests-ok` (ruleset `main merge queue`). A failing non-required check does not stop a queue entry. Adding `merge-gate` there is a ruleset change — the maintainer's, per AGENTS.md — so this PR ships the check and says so in rule 1 instead of pretending it already blocks. 2. **Review `state` is never read.** The account cannot approve its own PRs, so its reviews stay `COMMENTED` even for an INC; the INC on ThinkFlowLab#610 was a comment. Reading state would have let ThinkFlowLab#610 through, which is the whole point. This is the `merge_group` half. The enqueue-time self-check is Maintainer's `verdict-gate.mjs`; same rule, two places, stricter answer wins. ## Ran / not run - Ran: `node --test ".github/scripts/*.test.mjs"` — 49 pass, 0 fail (11 of them new). - Ran: YAML parse of the new workflow; `actionlint` could not be downloaded here (github.com release asset blocked from this host), so `package`'s actionlint step is the first real lint of it. - Not run, at all: `npm run verify`, the core suites and the smoke test. This host has no usable sandbox backend (`bash` refused with `SANDBOX_UNAVAILABLE` at first), the checkout was rebuilt from the API rather than cloned, and no dependency tree exists here. CI is the backstop for everything but the new script tests. - Docs-not-needed: policy text only; no subsystem owns `AGENTS.md` or `docs/autonomous-dev.md`.
…l a working session blocked (ThinkFlowLab#615) (ThinkFlowLab#621) The preflight merged in ThinkFlowLab#610 answers a question about a session it cannot see: it hardcoded `workspace-write`, so a `danger-full-access` session — where `bash` works — was told "every shell call will be refused", and `AGENTS.md` then says to stop and report a block. Windows sessions were reported blocked unconditionally, because the ACL runner was hardcoded to `false`. ## 修了什么(对照 ThinkFlowLab#615 的三项) 1. **模式来自调用方**:`--mode <read-only|workspace-write|danger-full-access>` 或 `FLYBOT_SANDBOX_MODE`(flag 优先)。`danger-full-access` 不需要任何后端,永远不会被判 blocked;`read-only` / `workspace-write` 才依后端探测。**不给模式时输出条件式措辞**("if this session is on workspace-write, every shell call is refused with it"),不冒充确定结论。 2. **第三态 `unknown`**:`acl-runner` 报 `unknown` 而不是 `false`,JSON 里多一个 `unknown: true`;退出码把「证明被挡」和「探不到」分开 —— **0 可用 / 3 证明被挡 / 4 探不到 / 2 用法**。`AGENTS.md`、`docs/sandbox.md`、`fix-issue` 的 SKILL 都写明 **exit 4 不许停手**:真正的证据是 `bash` 返回 `SANDBOX_UNAVAILABLE`。 3. **两个小项**:`describe()` 用 `result.unlock`(不再取模块常量);`resolveOnPath` 用 `statSync().isFile()`,PATH 里叫 `bwrap` 的目录不再被当成二进制;`docs/sandbox.md` 的 "it print" → "it prints"。 同时补了 `docs/sandbox.md` 的模式/退出码表与 `changelog.d/615-sandbox-preflight-unknown.fixed.md`。 **为什么是新分支**:原分支 `fix/607-sandbox-preflight` 在 INC 之后 46 秒被合入(`7eae271c`),已无法在其上继续;ThinkFlowLab#607 保持 closed,返工由这条 issue 承载(fix-forward,不 revert)。 ## 撤掉修复后变红 保留新测试,只把脚本的三个行为改回 main 的版本(模式硬编码 `workspace-write`、`acl-runner` 返回 `false`、未知一律按 blocked 退出、去掉 `isFile()` 检查): ```sh node --test .github/scripts/sandbox-preflight.test.mjs # tests 14 / pass 8 / fail 6 not ok - ThinkFlowLab#615: danger-full-access is never reported blocked, even with no backend at all not ok - ThinkFlowLab#615: the CLI exits 0 in a danger-full-access session on a host with no backend not ok - ThinkFlowLab#615: danger-full-access from the environment variable is honored like the flag not ok - ThinkFlowLab#615: the Windows ACL runner is unknown, so a Windows session is not called blocked not ok - ThinkFlowLab#615: with no policy given the CLI says it cannot tell and exits 4 not ok - ThinkFlowLab#615: a directory named bwrap on PATH is not taken for the binary ``` 用 main 的脚本直接替换(连导出都没有)时是文件级红:`# tests 1 / pass 0 / fail 1`。 ## 修复后变绿 ```sh node --test .github/scripts/sandbox-preflight.test.mjs # tests 14 / pass 14 / fail 0 ``` 对应 ThinkFlowLab#615 验收的四条用例:`--mode danger-full-access` → exit 0 且无 "will be refused";`--mode workspace-write` 无后端 → exit 3;`acl-runner` → `unknown: true`、exit 4;PATH 里的 `bwrap` 目录 → `bwrap: false`。 本机真跑(这台是 `danger-full-access`、无 bwrap): ```text $ node .github/scripts/sandbox-preflight.mjs --mode workspace-write # exit 3,一行 UNAVAILABLE $ node .github/scripts/sandbox-preflight.mjs --mode danger-full-access # exit 0,"needs no sandbox backend" $ node .github/scripts/sandbox-preflight.mjs # exit 4,"Do not stop on this: …" ``` ## 跑了什么 - `node --test .github/scripts/sandbox-preflight.test.mjs`:红 6 条 → 绿 14/14。 - 上面三条 CLI 调用(真机,exit 3 / 0 / 4)。 - `node .github/scripts/changelog.mjs check` → 23 fragments well formed。 ## 没跑什么 - `npm run verify` 整体与 core/ 套件:本 PR 只动 `.github/scripts`、`docs/`、`AGENTS.md`、SKILL 与 changelog 片段,没有 `core/` 源码改动;CI 在 tip 上跑 `tests-ok`(含 core 套件、lint、arch-docs、patch check)。 - `.github/scripts/*.test.mjs` 的整个 glob 与 actionlint:只跑了本文件。 - Windows 真机:`acl-runner` 的 unknown 分支由单元测试覆盖,没有在 Windows 上执行过。 - 没有在 `read-only` 会话里验证(本机无法切模式)。 ## 风险 `.github/scripts` 与文档;无产品代码、无密钥/权限/共享面。`risk:T*` 由 controller 按 diff 给。 Fixes ThinkFlowLab#615
…#670) Fixes ThinkFlowLab#609 Tip SHA: 4c8d98c ## Reproduce The rule text says `risk:T*` is set by the controller from the diff, never by hand, and the mechanism does not hold up: `github-actions[bot]` labelled ThinkFlowLab#574 at 07:28:43Z, while ThinkFlowLab#606, ThinkFlowLab#610, ThinkFlowLab#611, ThinkFlowLab#612, ThinkFlowLab#616, ThinkFlowLab#617, ThinkFlowLab#618 and ThinkFlowLab#619 carry no `risk:T*` at all even though `risk tier (report only)` succeeded on every one of them. The owner decision on the issue is ②: keep the workflow read-only, never label — and make the computed tier auditable instead of remembered. At this tip, `node .github/scripts/risk-gate.mjs --pr <n>` ends its report with risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3 so a review can quote the tier the rules produced instead of restating a number. ## What changed - `.github/scripts/risk-gate.mjs`: `judge()` records every changed path (and a rename's old path) with the tier `tierOf()` gave it, and `summaryLine()` renders the final line of the report from that same result — highest tier first, paths capped at 10 with a `+N more` tail. `render()` ends the job summary with it, after the prose, so the two can never disagree. - `.github/scripts/risk-gate.test.mjs`: four cases — the line's exact shape for a T3 PR, the ordering and the cap, the no-changed-files case, and "the line is the last line of the report". - `AGENTS.md` gains a bullet under the label section: quote the summary line, the workflow never writes a label, the controller copies it on its poll, no review transcribes a tier. - `.github/labels.json`: every `risk:T*` description keeps `advisory` and `the merge gate recomputes it` and now also names the controller as the writer (all ≤ 100 characters). - `docs/controller.md`: the tier stays auditable without the label — quote the summary line. - `.github/workflows/risk-tier.yml`: header comment only — no permission added, no step changed. It keeps `contents: read` and `pull-requests: read` and still never labels, comments or merges. ## What I ran - `TZ=UTC node --test .github/scripts/*.test.mjs` at this tip (with `git` on `PATH`) → **144/144**. - `node --test .github/scripts/risk-gate.test.mjs` → **18/18**, including the four new cases. - `node .github/scripts/risk-gate.mjs` behaviour checked by hand through the harness result: `judge([AGENTS.md, plugins/social/src/social.js])` → `risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3`; no files → `risk-tier: T0; basis: none (no changed files)`. - `changelog.mjs check` not run: a change users cannot see needs no fragment (changelog.d/README). ## Red without the source change - Removing the `basis.push([name, t])` line from `judge()` (the tests untouched) → `risk-gate.test.mjs` **16/18, 2 red**, exactly `summaryLine: one machine-readable line…` and `summaryLine: highest tier first…`. - Removing `summaryLine(result)` from the end of `render()` instead → **17/18, 1 red**, exactly `render: the machine-readable line is the last line of the report`. Restored → 18/18. ## Not run - The workflow itself (it needs Actions): the change to `risk-tier.yml` is a comment, and the existing `the workflow stays report only` test still passes with it. - `npm run verify` end to end: no `core/node_modules` in this sandbox (the same on `main`). - The `risk-tier` job on this PR runs the base copy of the script, so its summary will only show the new line after merge — that is the workflow's normal base-copy behaviour, not a gap here. ## Risk T3 by the repository's own rules (`.github/**`, `AGENTS.md`): a reporting script plus text. It adds no permission, no network call, no label, comment or merge, and cannot block anything — the job is still `continue-on-error: true` and not a required check. The only behaviour it changes is the last line of that job's summary. The one thing a reader must not conclude is that the label now arrives on time: it does not, and the docs keep saying why (ThinkFlowLab#629).
A session that starts on
workspace-writeon a host with no usable sandbox backend refuses everybashcall withSANDBOX_UNAVAILABLE, and the session only looks stuck. Nothing names the block in one line, so an agent retries and reports "the command failed".复现
On a host without bubblewrap and without Landlock (this host, 2026-10-21), any
bashcall underworkspace-writereturns:The same command under
danger-full-accessruns, so the host can run commands; only the confined path has no backend. Before this PR nothing aggregated that into a session-level status. The new test fails without the script: moving.github/scripts/sandbox-preflight.mjsaway gives# tests 1 / pass 0 / fail 1(the test file cannot import it).改了谁
.github/scripts/sandbox-preflight.mjs(new): detects the backends DSH's local chain would use (Linuxbwrapprobe with the real read-only profile, then Landlock from/sys/kernel/security/lsm; macOSsandbox-exec; the Windows ACL runner is not probeable from a Bot shell and reports unavailable). Prints one line plus the unlock, or--json; exit 0 usable, 3 blocked, 2 usage..github/scripts/sandbox-preflight.test.mjs(new): blocked host, runnable bwrap, enabled Landlock, and the CLI exit codes/message on a fakePATH.docs/sandbox.md(new): the permission-tier table (read-only/workspace-write/danger-full-access, approvals open/closed), what each can write, and each failure mode.AGENTS.md: run the preflight once at session start; when blocked, report one line and stop retrying shell calls (file tools and the GitHub API still work)..claude/skills/fix-issue/SKILL.md: the same instruction in the worktree/environment section.changelog.d/607-sandbox-preflight.changed.md.Harness side (named, not changed here): DSH owns setting the file policy and carrying
SANDBOX_UNAVAILABLEthroughtool/result. This PR adds FlyBot's detect-and-report half only; no DSH patch and no dependency on a DSH install.跑了什么
node --test .github/scripts/sandbox-preflight.test.mjs→ 5/5 pass.node .github/scripts/sandbox-preflight.mjson this host → exit 3 with the one-line block (quoted above);--jsongives{ "usable": false, "backends": { "bwrap": false, "landlock": false } }.node .github/scripts/changelog.mjs check→ 20 fragments well formed.撤掉源码改动后变红
With the script restored:
# tests 5 / pass 5 / fail 0.没跑什么
npm run verify(无 core/ 源码改动)、core/plugins 套件、.github/scripts整个 glob(只跑了本文件)、smoke、真机。CI 会在 tip 上跑tests-ok。风险
Docs and
.github/scriptsonly; no runtime code and no secret/network/sharing surface. (risk:T*is set by the controller.)Fixes #607