Skip to content

A forbidden input that only a document forbids is not a firewall - #814

Merged
MongLong0214 merged 2 commits into
mainfrom
cdeb-v3-pr2
Aug 20, 2026
Merged

A forbidden input that only a document forbids is not a firewall#814
MongLong0214 merged 2 commits into
mainfrom
cdeb-v3-pr2

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

PR 2 of CDEB-Fresh v3 (PRD §23): role governance, information firewalls, role prompt hashing, study/gold/task/run/patch schemas, the state machine and transition ledger, and cross-study ingestion refusal.

What is enforced, not just stated

§3.4 gives eighteen roles their allowed and forbidden inputs; §3.6 requires each prompt be versioned and hashed. This PR makes both executable.

  • roles.ts verifies every prompt's SHA-256 against the bytes on disk and refuses the manifest by name when one drifted. A prompt edited without updating its hash is a hard failure — a role whose prompt changed silently is a role whose forbidden inputs are unknown.
  • forbiddenInputViolations(roleId, offered) answers what a caller is about to hand a role, so later PRs gate on it instead of on care. Offering GOLD-A the record payload, or PATCH-A the arm label, is reported.

Two constraints that differ in kind

§3.8 (no self-approval) and §3.5 (model-family diversity) are modelled separately rather than as one boolean:

same session for a §3.8 pair    → invalid
same model family for a §3.5 pair → DOWNGRADE
                                    label becomes single-family-internally-replicated
                                    README headline eligibility HOLD

§3.5 permits a single-family study to run provided it is labelled and held. Refusing outright would lose a real study; passing silently would publish an overclaim.

Lifecycle and cross-study refusal

Thirteen forward-only states (§4.1). appendTransition refuses a backward move, refuses an artifact failing the schema, and refuses any row whose study_id belongs to another study, naming both ids. §5.2 forbids reusing old result rows — a v1.3 row must be unreadable as a v3 row, not merely unwelcome.

Schemas

Four join the two from PR 1: the §4.2 transition artifact, §21.2's run row, §21.3's patch audit row, and gold. Where §8 named a field without fixing its labels, the narrower enum was chosen so a later author cannot introduce an unreviewed semantic category by writing one.

Verification

48 cases across three v3 suites, tsc --noEmit clean. Four negative controls, each observed failing and then restored:

canTransition accepts a backward move        → lifecycle test fails
required field removed from run-row schema   → rejection test fails
one character appended to gold-a.md          → hash check fails, naming GOLD-A
forbidden input removed from GOLD-A entry    → firewall test fails

Limits, in the commit

The firewall checks what a caller declares it is offering. A caller that mislabels an input passes, so this bounds accident and not deception; the sealed-store split bounds the latter. Prompt hashes bind the prompt text, not the context a runner assembles — PR 5 is where the assembled context becomes observable.

PR 2 of CDEB-Fresh v3 (PRD §23). The study's independence rests on eighteen
roles that each must not see certain things: GOLD-A must not see the record
payload it is reconstructing, PATCH-A must not see the arm it is judging,
STAT-B must not see STAT-A's code. §3.4 states those inputs; §3.6 requires each
prompt be versioned and hashed. Stated is not enforced, so this adds the
enforcement.

`roles.ts` verifies every prompt's SHA-256 against the bytes on disk and refuses
the manifest by name when one drifted. An edited prompt with a stale hash is a
hard failure rather than a warning: a role whose prompt changed silently is a
role whose forbidden inputs are unknown. `forbiddenInputViolations` answers what
a caller is about to hand a role, so later PRs gate on it instead of on care.

§3.5's model-family requirement and §3.8's no-self-approval rule differ in kind
and are modelled that way. Reusing a session for a pair is invalid. Sharing a
model family is a DOWNGRADE: the study still runs, the label becomes
single-family-internally-replicated, and README headline eligibility is HOLD.
Collapsing those two into one boolean would have made an honest downgrade look
like a failure and tempted the opposite.

The lifecycle is thirteen forward-only states. `appendTransition` refuses a
backward move, refuses an artifact that fails the schema, and refuses any row
whose `study_id` belongs to another study, naming both ids -- §5.2 forbids
reusing old result rows, and a v1.3 row must be unreadable as a v3 row rather
than merely unwelcome.

Four schemas join the two from PR 1: the §4.2 transition artifact, §21.2's run
row, §21.3's patch audit row, and gold. Where §8 named a field without fixing
its labels, the narrower enum was chosen so a later author cannot introduce an
unreviewed semantic category by writing one.

Record-Id: r-v3rolegov
Provenance: authored
Certainty: firm
Blast: module
Undo: easy
Ruled-out: keeping the role manifest as documentation and checking it in review | the manifest lists what each role must not see, and a list nobody executes is satisfied by any behaviour at all
Ruled-out: treating a same-model-family pair as an error | §3.5 permits the study to run single-family and requires it be labelled and held; refusing outright would lose a real study, and passing silently would publish an overclaim
Ruled-out: comparing the role list in the test against a second list written beside it | a copy agrees with its original and proves nothing, so completeness is read from the PRD table
Limit: the firewall checks what a caller declares it is offering a role. A caller that mislabels an input passes, so this bounds accident and not deception, and the sealed-store split is what bounds the latter
Limit: prompt hashes bind the prompt text, not the context a runner actually assembles; PR 5 is where the assembled context becomes observable
Verified: 48 cases across three v3 suites, tsc clean. Four negative controls, each observed failing and then restored -- a backward transition accepted by canTransition, a required field removed from run-row.schema.json, one character appended to gold-a.md, and one forbidden input removed from GOLD-A's manifest entry
CommitLore-Version: 2.0.0
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 2 commits in origin/main..5230bdf4f1df4ebc0de693064cd1636b27138cbc
Active constraints: 14 limits · 29 ruled-out · 8 warnings — from 12 records over 32 changed paths

Active constraints for the paths this PR touches

Limits (14)

  • r-v3schemaauth 5230bdf — the shape guard binds the field set, not the field semantics; §8.8 could rename a field's meaning while keeping its name and nothing here would notice
  • r-v3rolegov d67eb7a — the firewall checks what a caller declares it is offering a role. A caller that mislabels an input passes, so this bounds accident and not deception, and the sealed-store split is what bounds the latter
  • r-v3rolegov d67eb7a — prompt hashes bind the prompt text, not the context a runner actually assembles; PR 5 is where the assembled context becomes observable
  • r-v3authoritypr1 2746dbd — the schemas constrain the shape of a source and a verdict, not their truth. A blog summary recorded as source_kind: publisher with a real digest passes every check here; §2.1's ban is on what the source IS, and only the auditor reading it can tell
  • r-cdeb09sm 74d9c62 — this exercises the attacks the design anticipated, and an adversary who reads it will look for the ones it does not attempt -- notably collusion between a candidate and an evaluator image, and any attack on the freeze itself before a run starts
  • r-cdeb07or 5607dd5 — the state machine cannot distinguish an agent that produced no model turn from one whose host failed to report the turn it produced, so it treats both as interrupted and neither as retryable -- a real pre-turn infrastructure failure is therefore sometimes charged as a lost run
  • r-cdeb07or 5607dd5 — atomicity rests on rename and fsync semantics of the underlying filesystem; a filesystem that reorders them can still present a partial the recovery pass reads as whole
  • r-cdeb05lg 5d347b9 — the ledger observes only bytes emitted before the agent process closes -- terminal usage never written to the stream, external billing, and work outside that stream are all unavailable to it rather than estimated
  • r-v13fix f4b6859 — schemas and the verifier now require the qualification evidence, but nothing yet produces it -- CDEB-10 must build the probe and the deliverability check before a freeze can be written
  • r-cdebpres e01b482 — one repository, four tasks, six usable runs per arm and a local evaluator -- no claim gate is evaluable and preregistration §8 forbids any number here reaching a public surface
  • r-cdebver01 ce7b278 — the schemas freeze protocol 1.2.0 constants -- thresholds, matrix size, category names -- so a protocol change is a schema change and CI notices
  • r-benchscope 67f4375 — nothing checks the shape of the eight metric-row files. This gate names them and steps over them, and bench/deterministic/types.ts is the only definition that family has -- there is no JSON schema for it, so drift on that side is still invisible
  • r-benchscope 67f4375 — the pre-provenance exemption reads started_at, which is data on the row rather than a fact about the file. A row that misreported it would be held to the shorter list of requirements; that is a deliberate falsification rather than the omission this fixes, and nothing here detects it
  • r-6c2b95 5da793c — the CLI holds no API key, so any real driver runs through the user's own agent session and cannot be exercised in this environment

Ruled out (29)

  • r-v3schemaauth 5230bdf — relaxing bench/tsconfig.json so the plugin import compiles | that alters a build contract to accommodate one import, and the module settings are not the thing that is wrong
  • r-v3schemaauth 5230bdf — casting the plugin to a callable | it hides the interop mismatch rather than removing it, and the cast would outlive whoever understood it
  • r-v3schemaauth 5230bdf — keeping the widened patterns and noting the placeholder exception in a comment | a comment does not stop a validator from accepting the word "40-hex" where a commit id belongs
  • r-v3rolegov d67eb7a — keeping the role manifest as documentation and checking it in review | the manifest lists what each role must not see, and a list nobody executes is satisfied by any behaviour at all
  • r-v3rolegov d67eb7a — treating a same-model-family pair as an error | §3.5 permits the study to run single-family and requires it be labelled and held; refusing outright would lose a real study, and passing silently would publish an overclaim
  • r-v3rolegov d67eb7a — comparing the role list in the test against a second list written beside it | a copy agrees with its original and proves nothing, so completeness is read from the PRD table
  • r-v3authoritypr1 2746dbd — deleting the v1.3 PRD | its pilot and its failures are the reason v3's gates exist, and a protocol whose predecessor is unreadable cannot be checked against what it learned
  • r-v3authoritypr1 2746dbd — seeding the literature lock with sources from the v3 PRD's own tables | those tables are the claim, and populating the lock from them would make the matrix cite itself
  • r-v3authoritypr1 2746dbd — keeping .gitkeep in directories that PRD §21 gives real files | the placeholder and the file would then both be present, and the tree would stop matching the document it is checked against
  • r-cdeb09sm 74d9c62 — relaxing the analyzer to accept rows discovered on disk | it would have closed the wiring gap by removing the guarantee that closes bench:m5 reads every jsonl in bench/results, so the registered analysis would run over M1, M4 and files marked non-citable #441
  • r-cdeb07or 5607dd5 — retrying an agent after a post-turn infrastructure failure | keeping whichever attempt survived selects on the outcome, and the surviving attempt is not a random draw
  • r-cdeb07or 5607dd5 — re-deriving the candidate tree for an evaluator retry | it converts an evaluator flake into a second candidate, which is a different experiment
  • r-cdeb05lg 5d347b9 — inferring terminal usage from the turns that did arrive | it would be a plausible number carrying no evidence, and plausible is exactly what makes it survive review
  • r-cdeb05lg 5d347b9 — excluding an unavailable run from the denominator | that silently redefines the population the token claim describes
  • r-v13fix f4b6859 — keeping the derived threshold with the exponent corrected | any threshold that moves with measured overhead is a threshold the study can influence, and the fixed bar is the only one that means the same thing before and after the run
  • r-v13fix f4b6859 — lowering 15% to something the pilot's overhead clears | that measures a different claim and calls it the same one; if 45% overhead makes the bar hard, the bar is reporting a true fact about the product
  • r-cdebpres e01b482 — reporting the completed-only table as the pilot's result without the timed-out task | the exclusion is the registered stop_reason rule, and a reader who cannot see that a quarter of the study never finished cannot judge the rest
  • r-cdebpres e01b482 — dropping the two zero-delivery tasks as broken | they are the intention-to-treat case the protocol is built around, and they are also how the qualification gap was found
  • r-cdebver01 ce7b278 — default-in discovery over the CDEB tree like the legacy gate | that gate's enemy was the unregistered file, CDEB's is the unregistered input, and bench:m5 reads every jsonl in bench/results, so the registered analysis would run over M1, M4 and files marked non-citable #441 showed a glob passing a stopping rule on contamination
  • r-cdebver01 ce7b278 — validating with the draft-07 Ajv entry and downgrading the schemas | the 2020-12 entry ships in the same package, and weakening six schemas to fit a meta-schema is backwards
  • r-benchscope 67f4375 — A declared list of result files, in the gate or in a manifest | opt-in under another name, and a file left off it is silently ungated -- which is precisely the failure bench/verify.mjs is not run by anything, so the result schema drifted unnoticed #392 reports
  • r-benchscope 67f4375 — Validating only the three files that pass today | a description of the present rather than a rule, and it goes stale the first time a matrix is added or the schema moves
  • r-benchscope 67f4375 — A naming convention on results filenames | the runner's default output is an ISO stamp plus hex and the metric writers' is a family name plus an ISO stamp, but every run-record file in the tree was named by hand through --out (t702-m2, m5-off-design-20-tasks), so the convention is enforced nowhere and cannot carry a gate
  • r-benchscope 67f4375 — A marker file dropped beside each gated result | still a step somebody has to remember, and a forgotten step is the defect being fixed
  • r-benchscope 67f4375 — Widening result.schema.json to describe both row families | they share no required field, so the union would accept a run record with no task and a metric row with no metric, which is a schema that rejects nothing
  • r-benchscope 67f4375 — Making harness_commit and dist_digest optional in the schema | weakens the gate for every future row in order to admit six historical files, and Make per-turn token usage observable, and say what it still cannot price #390's drift was an additionalProperties failure that a laxer schema catches later rather than sooner
  • r-benchscope 67f4375 — Skipping the six pre-provenance files whole | cheaper to write, but it stops checking types, patterns and the closed property set on 380 rows in order to excuse two absent fields
  • r-6c2b95 5da793c — skip the dry-run driver | then nothing exercises the harness until a key exists, and the first real run debugs the harness instead of measuring anything
  • r-6c2b95 5da793c — emit dry-run rows without a marker | indistinguishable from measurements the moment they leave the terminal

Warnings (8)

  • r-cdeb09sm 74d9c62 (claim) — the container runtime was unavailable here, so network isolation, host secret and mount isolation, and resource limits are proven only against the local sealed evaluator; the OCI enforcement path needs a machine with a working daemon before the freeze may treat it as tested
  • r-cdeb07or 5607dd5 (claim) — the analyzer reads only row files the freeze names, so the freeze must publish the full set of canonical row paths and this orchestrator must write rows at exactly those paths; the two are wired together by the freeze and nothing checks the pairing yet
  • r-cdeb05lg 5d347b9 (claim) — one unavailable run makes the whole aggregate unavailable, which is the strict reading of §14.6 and will feel severe on a long matrix; the reasons are per run, so a report can say which ones and why
  • r-cdeb05lg 5d347b9 (claim) — the artifact path uses node:zlib zstd, which exists from Node 22.15 -- above the package floor of 22, though this is bench-only and no shipped code imports it
  • r-v13fix f4b6859 (claim) — the 0.6 probe budget is still a judgement call with no measurement behind it, and it now gates corpus membership through two agent sessions per task
  • r-cdebpres e01b482 (claim) — the exposure counter counts deliveries rather than opportunities, so a zero cannot distinguish a hook that never fired from one that fired on a path with no records -- CDEB v1 §9.5 requires that split and CDEB-05 must build it
  • r-cdebver01 ce7b278 (claim) — every failure fixture is the one valid row with a single mutation, so keep the valid-row builder in lockstep with result.schema.json -- a builder drift fails all eleven cases at once and reads as a broken verifier
  • r-6c2b95 5da793c (claim) — never cite a row with simulated:true -- README numbers come from bench/results logs and those rows are not results

git log --follow accepts exactly one pathspec, so renames are not followed for 32 paths; query one path at a time to follow its rename chain

Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR.

…ess for a template

Three corrections to PR 2, each found by running what CI runs rather than what I ran.

The bench typecheck. `bench/tsconfig.json` sets `verbatimModuleSyntax`, under which
`ajv-formats`' default import resolves to an uncallable module namespace. The root
typecheck was clean and the bench one was not; CI runs both and I ran one. The plugin is
gone from `bench/`: the only format the transition schema uses is `date-time`, and it is
now registered as an explicit rule this repository can read. A static assertion fails on
the machine that writes such an import instead of ten minutes later.

The gold schema required six fields §8.8 does not print -- study_id, record_id, the two
annotator ids, the adjudicated resolution and the source-packet digest. They came from my
instruction, derived from §8.2 and §8.4, and they made the PRD's own example invalid under
the schema meant to serve it. The PRD is the sole authority for this implementation, so
its printed shape is the required shape; those six are typed and optional now.

Then the guard for that. I asked for §8.8's example to validate, and §8.8 is a template
whose values are placeholders -- "40-hex", "64-hex", "r-...". The only way to satisfy that
is to widen the patterns, which is what happened: `^(?:[0-9a-f]{40}|40-hex)$` would have
let a real gold file record its snapshot as the word "40-hex". The instruction was wrong,
not the constraint. The guard now compares the schema's required set against the field
names §8.8 prints and reports the difference in both directions, which catches a schema
stricter than its authority and one looser than it. Values are exercised where values
belong: against a real instance.

The lesson is the one this PR keeps re-teaching. A check that names the wrong target does
not merely fail to help -- it pushes the implementation toward the wrong property, and the
implementation obliges.

Record-Id: r-v3schemaauth
Provenance: authored
Certainty: firm
Blast: module
Undo: easy
Ruled-out: relaxing bench/tsconfig.json so the plugin import compiles | that alters a build contract to accommodate one import, and the module settings are not the thing that is wrong
Ruled-out: casting the plugin to a callable | it hides the interop mismatch rather than removing it, and the cast would outlive whoever understood it
Ruled-out: keeping the widened patterns and noting the placeholder exception in a comment | a comment does not stop a validator from accepting the word "40-hex" where a commit id belongs
Limit: the shape guard binds the field set, not the field semantics; §8.8 could rename a field's meaning while keeping its name and nothing here would notice
Verified: 51 cases across three v3 suites, and both typechecks -- root and bench -- clean. Three negative controls, each observed failing and then restored: an ajv-formats import re-added under bench/, a field added to the schema's required set that §8.8 does not print, and the real gold instance's snapshot_sha set to the word "40-hex", which the restored pattern refuses. bench/verify.mjs still validates 1902 rows across 17 files
CommitLore-Version: 2.0.0
@MongLong0214
MongLong0214 merged commit b1d4c08 into main Aug 20, 2026
14 checks passed
@MongLong0214
MongLong0214 deleted the cdeb-v3-pr2 branch August 20, 2026 22:05
MongLong0214 added a commit that referenced this pull request Aug 21, 2026
…hing was

A corrective review measured the study's state against its own claims and found the
state ahead of the evidence. `transitions.jsonl` recorded

    DRAFT -> LITERATURE_LOCKED   actor UNKNOWN
    checks: ["STATUS.json records phase literature-lock",
             "git commit 2746dbd created STATUS.json"]

on a tree where source-lock held 0 sources, the evidence matrix 0 claims, and the audits
directory was empty. The checks are circular: both cite the artifact that asserts the
lock. Evidence for a lock cannot be the file claiming it.

That defect is worse than one wrong row. A LITERATURE_LOCKED that passes on empty
artifacts makes CORPUS_QUALIFIED pass on empty artifacts too, and the state machine stops
being a gate and becomes a label.

The row is preserved rather than corrected. Editing a ledger to fix a ledger produces
exactly the artifact an auditor cannot distinguish from the original mistake, so
`cdeb-fresh-v3` moves to a new terminal INVALIDATED state, the deviation carries the
offending line's bytes and its sha256 beside the counts that contradict it, and
`cdeb-fresh-v3r1` starts at DRAFT with an empty selection and a null seed.

Entering LITERATURE_LOCKED now measures what it asserts: sources above zero, claims above
zero, LIT-A/LIT-B/LIT-C artifacts present, unresolved claims zero, and a named actor. A
check whose text contains its own destination state is refused as circular, which is the
specific shape that got through.

Three provenance leaks close with it. Candidates were written as `benchmark: "cdeb-v1"`
with no study_id, so a legacy row and a fresh one were indistinguishable by contract and
PR #814's cross-study refusal never reached them; the v3 contract requires study_id and
source_snapshot_sha and refuses the legacy field outright. The census passed no exclusion
list, leaving the enumerator's benchmark-authored option unused; a fifteen-row exclusion
index now gates it, each row carrying the path or commit that evidences it rather than an
inference from how a record id looks. And the census read whatever `dist` the working tree
happened to hold while the manifest claimed a release, so the release digest is recorded
and checked.

One guard here was written too wide and had to be narrowed. Asserting that no personal
path appears anywhere under `bench/` made four historical runner logs lose the path a past
run actually wrote to. Those logs are evidence of what happened; a guard that forces
recorded history to be rewritten is worse than the path it removes. They are restored, and
the assertion now scans code and active-study artifacts with `bench/results/` and
`bench/cdeb/archive/` named as explicit exclusions, so widening it later is a visible edit.

No measured data existed while any of this was true. Selection is empty, rows and pilot
are empty, the seed is null and measured runs remain disallowed, which is the only reason
the study is recoverable rather than lost.

Record-Id: r-v3corrective
Provenance: authored
Certainty: firm
Blast: system
Undo: costly
Ruled-out: editing the false transition in place | the edit is indistinguishable from the mistake it repairs, and an auditor reading the ledger afterwards cannot tell which they are looking at
Ruled-out: keeping one study instance and rewinding its state | the lifecycle is forward-only by design, and a rewind is the same lie as the premature advance with the sign flipped
Ruled-out: renaming the benchmark field and keeping the v1 contract | the field is not the problem; a contract that cannot distinguish a legacy candidate from a fresh one is
Ruled-out: inferring benchmark-authored status from record id spelling | a naming convention is not evidence of intent, and the review names this shortcut explicitly
Ruled-out: rewriting the historical runner logs to satisfy the path guard | those bytes record where a run actually wrote, and a guard is not a licence to change what happened
Limit: the exclusion index is only as complete as the evidence found for it; one row is recorded as ambiguous-pending-adjudication rather than asserted either way, and an unknown old artifact that left no trace in this repository cannot be excluded by it
Limit: the literature lock is still not done. This makes the gate real; it does not populate the sources, and the active study stays at DRAFT until LIT-A/B/C actually run over originals
Limit: the release binding checks the digest this repository can compute today; a release whose shipped artifact cannot be reproduced locally is out of its reach
Verified: 3496 cases pass across the full suite with 13 skipped, both typechecks clean, and the four v3 suites green at 72. Negative controls observed and restored: a source added while claims stayed empty is still refused, a circular check passes only with the guard disabled, a legacy cdeb-v1 row is refused by identity, a deleted exclusion index stops the census, a personal path in census.ts fails the guard while the same string inside bench/results does not, and git status shows bench/results unchanged
CommitLore-Version: 2.0.0
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.

1 participant