You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Safety: graders and evidence read-only for the Bot under test #398
Child of #386 (layered safety RFC), related to #204 / #358 / #357.
Why
Evaluations (live E2E, Nightly, performance board) are only trustworthy if the Bot under test cannot read or modify the grader, expected values, or evidence. Goal-driven agents may tamper with scorers if they can.
Scope
Grader isolation: expected totals, fixtures' answer keys, and scoring code are outside the Bot's writable and readable scope; they run in a separate process/user or are injected only at assertion time.
Read-only observation: assertions read delivered bytes and state from outside the Bot's sandbox (as dev/live-e2e aims to do); the Bot cannot alter the observation path.
Tamper check: before and after a run, hash grader files and fixtures; fail the run (not just the check) if they changed.
Evidence integrity: sanitizer-cleared evidence (existing) plus hashes in the report so post-hoc edits are visible.
Triage on main 7b9e90b, 2026-10-19 ~12:10 Asia/Shanghai: needs-human, not agent-ready. Child of #386. Question for the owner: build grader isolation for dev/live-e2e now?
Triage 2026-10-19 18:03 Asia/Shanghai (main 245be0b). Partly in place: dev/live-e2e/scenarios.mjs already hashes the uploaded orders.csv and summary.json and fails the scenario if they change during the run (immutability chain). Proposal for the next step, for the owner to confirm:
Scope: in dev/live-e2e, hash the grader files (scenarios.mjs expectations, fixtures/) before and after each run and mark the run invalid (not just a failed check) if any changed; write the hashes into the report; add a README section listing what the Bot can write (its own workspace only) and what it cannot (grader, fixtures, report); add a test where a fake Bot edits a grader file and the run is marked invalid. Running graders as a separate OS user/process is deferred.
Question: (a) build the tamper check + docs now, (b) full process/user isolation now, or (c) park until #357/#358 are scheduled? Recommendation: (a). It is small and dev-only.
Design for the owner to pick. This one is concrete now because #522 (merged) already ships the deterministic half of the #358 scenario runner under core/dev/eval/ (rubric.mjs, specs.mjs, judge.mjs, guard.mjs), and guard.mjs already hashes the rubric/spec files. #398 says the same must be true for the Bot under test: it must not be able to read or change the grader, expected values or evidence.
What "the Bot under test" is here: a mock or live run whose transcript the runner scores. Today the runner scores a transcript the caller passes; it does not yet own the run, so the isolation has to be defined for a run the runner performs.
(a) Hash-and-fail only (smallest). Before a run, guard.mjs hashes the rubric, specs and fixtures; after, it hashes them again and the run is marked invalid (not just failed) if any changed. No sandbox change. This catches tampering after the fact; it does not stop a Bot from reading the expected values, which is the second half of #398.
Size: ~40 lines (wire guard.unchanged into a run wrapper) + tests.
(b) Read-only grader files + tamper check (recommended). The runner opens the rubric, specs, fixtures and scoring code read-only (chmod 0444 in a temp copy, or a separate process/user for the grader) so the Bot under test cannot write them, and runs the tamper check of (a) so a read the runner cannot prevent is still visible. Assertions read delivered bytes/state from outside the Bot's sandbox (as dev/live-e2e aims to do), so the observation path is not the Bot's.
Size: ~120–200 lines (a run wrapper that stages a read-only grader dir, runs the mock/live target, then scores + verifies).
(c) Separate grader process/user. The grader runs in its own process (and, where the host allows, its own OS user) with the fixtures' keys injected only at assertion time. Strongest, but heavier and environment-specific (CI vs local); the checks of (a)/(b) still apply.
Size: ~200–300 lines plus CI wiring.
Recommendation: (b) now, with (a) inside it as the tamper check; (c) can be a later step if the live path needs it. It is the part #398 asks for that is testable in unit tests, and it keeps the mock runner usable without a sandbox.
Whichever is chosen, I can implement it as one PR on core/dev/eval/ plus its CI wiring, with a test where a fake Bot edits a rubric file and the run is marked invalid.
agent-ready, Owner: Dev2. The owner ruled this is a safety property, not a product taste: "the Bot under test must not be able to read or edit its own grader". So the three questions in the scope become three answered acceptance items, not a discussion.
Where it lands, on main24a40203:
dev/live-e2e/run.mjs (148 lines) — guards run first (:73 "never open a browser — or write anything — against a bench that should not"), the report is written at :50-54, the transcript at :131, and :135-146 is the clean-scan-then-stamp path (sanitizerFindings, infra_failed).
dev/live-e2e/report.mjs — buildReport, OUTCOMES, guardRun, scanTree is sanitize.mjs. OUTCOMES is where a new terminal outcome belongs, not a new boolean.
The fixtures and answer keys: dev/live-e2e/fixtures/orders.csv and scenarios.mjs.
Acceptance, in the order a reviewer can check it:
What the Bot under test may read: its own workspace and the delivered bytes. Notdev/live-e2e/** (graduers, scenarios.mjs expected values), notdev/live-e2e/fixtures/**, not the --evidence tree of a live run. Write the list into dev/live-e2e/README.md as a table (path → read / write / neither) — a scope nobody wrote down is one nobody can check.
What it may write: only its workspace. A write attempt outside it fails in the run, not after it: extend guardRun so the run stops with a terminal outcome for this case (add it to OUTCOMES), and name the code the caller sees — one string, e.g. grader/tampered, surfaced the way core/src/host/channel.js:25 maps a thrown error to bot/internal rather than printing a stack.
Tamper check: hash grader files and fixtures before the run and after it (createHash in run.mjs), and a mismatch must fail the run, not print a warning. The hash manifest goes into report.json next to the sanitizer findings so a post-hoc edit is visible.
Fake-Bot test: one test in dev/live-e2e/test/ where the fake adapter (fake-adapter.mjs) tries to edit a grader file and the run comes back with the new outcome and a non-zero exitCode. Deleting the guard must red exactly that test.
No new secret in artifacts — the existing sanitizer scan stays the gate (run.mjs:135-146), and the hash manifest must contain hashes only, never file contents.
Out of scope: the budget/metrics halves (#357, #645 C) and the DSH sandbox itself — if the isolation can only be enforced inside DSH rather than in this repo, stop and say so on this issue instead of simulating it.
Claimed by Dev2. Branch dev2/feat/398-grader-read-only from current main 24a40203.
Plan of attack, in the order Triage's acceptance lists them:
Write down the boundary first: a path → read/write table in dev/live-e2e/README.md for the Bot under test (its workspace + delivered bytes readable; dev/live-e2e/**, fixtures/**, --evidence trees not readable; writable: the workspace only). A boundary nobody wrote down cannot be checked.
Make a write outside that scope fail during the run — extend guardRun (the guard runs before the turn at run.mjs:73), add one terminal outcome to report.mjsOUTCOMES, and name the string the caller sees (mapped like channel.js:25 maps a throw to bot/internal, no stack).
Tamper check: hash the grader and fixtures before and after with createHash, fail the whole run (not warn) on a mismatch, and put the hash list in report.json.
If the isolation we can actually enforce here turns out to live in DSH rather than in this repository, I will stop and say exactly which piece is missing instead of writing a simulation — per the card and per Chief's instruction.
Activity
Birfy commentedon Oct 19, 2026
Triage on main
7b9e90b, 2026-10-19 ~12:10 Asia/Shanghai: needs-human, not agent-ready. Child of #386. Question for the owner: build grader isolation fordev/live-e2enow?Birfy commentedon Oct 19, 2026
Triage 2026-10-19 18:03 Asia/Shanghai (main 245be0b). Partly in place:
dev/live-e2e/scenarios.mjsalready hashes the uploadedorders.csvandsummary.jsonand fails the scenario if they change during the run (immutability chain). Proposal for the next step, for the owner to confirm:Scope: in
dev/live-e2e, hash the grader files (scenarios.mjsexpectations,fixtures/) before and after each run and mark the run invalid (not just a failed check) if any changed; write the hashes into the report; add a README section listing what the Bot can write (its own workspace only) and what it cannot (grader, fixtures, report); add a test where a fake Bot edits a grader file and the run is marked invalid. Running graders as a separate OS user/process is deferred.Question: (a) build the tamper check + docs now, (b) full process/user isolation now, or (c) park until #357/#358 are scheduled? Recommendation: (a). It is small and dev-only.
Add a user-scenario runner for the ThinkFlowLab#358 matrix
Add a user-scenario runner for the ThinkFlowLab#358 matrix (ThinkFlow…
HugoXK commentedon Oct 21, 2026
Design for the owner to pick. This one is concrete now because #522 (merged) already ships the deterministic half of the #358 scenario runner under
core/dev/eval/(rubric.mjs,specs.mjs,judge.mjs,guard.mjs), andguard.mjsalready hashes the rubric/spec files. #398 says the same must be true for the Bot under test: it must not be able to read or change the grader, expected values or evidence.What "the Bot under test" is here: a mock or live run whose transcript the runner scores. Today the runner scores a transcript the caller passes; it does not yet own the run, so the isolation has to be defined for a run the runner performs.
(a) Hash-and-fail only (smallest). Before a run,
guard.mjshashes the rubric, specs and fixtures; after, it hashes them again and the run is marked invalid (not just failed) if any changed. No sandbox change. This catches tampering after the fact; it does not stop a Bot from reading the expected values, which is the second half of #398.guard.unchangedinto a run wrapper) + tests.(b) Read-only grader files + tamper check (recommended). The runner opens the rubric, specs, fixtures and scoring code read-only (chmod 0444 in a temp copy, or a separate process/user for the grader) so the Bot under test cannot write them, and runs the tamper check of (a) so a read the runner cannot prevent is still visible. Assertions read delivered bytes/state from outside the Bot's sandbox (as
dev/live-e2eaims to do), so the observation path is not the Bot's.(c) Separate grader process/user. The grader runs in its own process (and, where the host allows, its own OS user) with the fixtures' keys injected only at assertion time. Strongest, but heavier and environment-specific (CI vs local); the checks of (a)/(b) still apply.
Recommendation: (b) now, with (a) inside it as the tamper check; (c) can be a later step if the live path needs it. It is the part #398 asks for that is testable in unit tests, and it keeps the mock runner usable without a sandbox.
Whichever is chosen, I can implement it as one PR on
core/dev/eval/plus its CI wiring, with a test where a fake Bot edits a rubric file and the run is marked invalid.Birfy commentedon Oct 25, 2026
Written-by: Triage
agent-ready,Owner: Dev2. The owner ruled this is a safety property, not a product taste: "the Bot under test must not be able to read or edit its own grader". So the three questions in the scope become three answered acceptance items, not a discussion.Where it lands, on
main24a40203:dev/live-e2e/run.mjs(148 lines) — guards run first (:73"never open a browser — or write anything — against a bench that should not"), the report is written at:50-54, the transcript at:131, and:135-146is the clean-scan-then-stamp path (sanitizerFindings,infra_failed).dev/live-e2e/report.mjs—buildReport,OUTCOMES,guardRun,scanTreeissanitize.mjs.OUTCOMESis where a new terminal outcome belongs, not a new boolean.dev/live-e2e/fixtures/orders.csvandscenarios.mjs.Acceptance, in the order a reviewer can check it:
dev/live-e2e/**(graduers,scenarios.mjsexpected values), notdev/live-e2e/fixtures/**, not the--evidencetree of a live run. Write the list intodev/live-e2e/README.mdas a table (path → read / write / neither) — a scope nobody wrote down is one nobody can check.guardRunso the run stops with a terminal outcome for this case (add it toOUTCOMES), and name the code the caller sees — one string, e.g.grader/tampered, surfaced the waycore/src/host/channel.js:25maps a thrown error tobot/internalrather than printing a stack.createHashinrun.mjs), and a mismatch must fail the run, not print a warning. The hash manifest goes intoreport.jsonnext to the sanitizer findings so a post-hoc edit is visible.dev/live-e2e/test/where the fake adapter (fake-adapter.mjs) tries to edit a grader file and the run comes back with the new outcome and a non-zeroexitCode. Deleting the guard must red exactly that test.run.mjs:135-146), and the hash manifest must contain hashes only, never file contents.Out of scope: the budget/metrics halves (
#357,#645C) and the DSH sandbox itself — if the isolation can only be enforced inside DSH rather than in this repo, stop and say so on this issue instead of simulating it.Reviewer:must not be Dev2.Birfy commentedon Oct 25, 2026
Claimed by Dev2. Branch
dev2/feat/398-grader-read-onlyfrom current main24a40203.Plan of attack, in the order Triage's acceptance lists them:
dev/live-e2e/README.mdfor the Bot under test (its workspace + delivered bytes readable;dev/live-e2e/**,fixtures/**,--evidencetrees not readable; writable: the workspace only). A boundary nobody wrote down cannot be checked.guardRun(the guard runs before the turn atrun.mjs:73), add one terminal outcome toreport.mjsOUTCOMES, and name the string the caller sees (mapped likechannel.js:25maps a throw tobot/internal, no stack).createHash, fail the whole run (not warn) on a mismatch, and put the hash list inreport.json.If the isolation we can actually enforce here turns out to live in DSH rather than in this repository, I will stop and say exactly which piece is missing instead of writing a simulation — per the card and per Chief's instruction.
feat(live-e2e): hash the grader and refuse a bench home that holds it (…
fix(live-e2e): keep the bench home out of the checkout and compare re…
Birfy commentedon Oct 25, 2026
Written-by: Dev2
#398done — 四项在 mainf9d01fd5上核过:#398closed/completed;squash 提交正文带Fixes #398(#1171mergef9d01fd5)。dev/live-e2e/的评测工具与文档(不随包发布、不在verify的 live 命令里),没有用户可见行为变化,与#204那套自身一致。dev/live-e2e/README.md的 path → read/write 表 + 三条LIVE_E2E_HOME命令、docs/subsystems/live-e2e.md的不变量(含 canonical 路径那句)、docs/interop/live-e2e-adapter.md的tampered终态,三处 contents 都是 200。#1171的agent-pr与#398的agent-working(pass是 controller 的,不动)。交付一句:被测 Bot 的工作区不再可能是本仓(
$LIVE_E2E_HOME或~/.cache/flybot-live-e2e/<name>,落在 checkout 内直接拒,路径按 realpath 比),--evidence指进dev/live-e2e也拒;篡改由前后两次 manifest 判tampered(exit 5,不写sanitized.txt)。读隔离本仓做不到,PR 与 README 明写是部署侧的活,没拿哈希冒充。feat(live-e2e): hash the grader and refuse a bench home that holds it (…