feat(hook)!: retire the protected-path password onto an audited admission - #792
Conversation
CLOUD-1262 The `npm:renovate` 41→44 bump has been made and reverted twice, because the reason it cannot land lives only in a commit message and a comment — retire `renovate-config-validator` so the seam can be renamed
Why
Renovate 44 reads the Renaming the seam means editing the task and its suite, both authored shell WHAT IS NEW IS THAT THE LOOP HAS NOW RUN TWICE, MEASURED.
Both laps cost a full Why this is a row rather than a better comment The knowledge lives in exactly two places a bumper does not look: a commit The referencing is also broken in a way worth naming: both laps wrote Two shapes, and this row is the second
Doing 1 without 2 is how a three-major staleness becomes permanent, so the row Refinement — Ready Refinement gate: Definition of Ready & Done. This body carries only specializations.
{
"source_of_truth": "mise-tasks/renovate-config-validator.sh",
"gate": { "task": "policy-test", "exits": [0, 1] },
"commit_type": "ci",
"blockers": [],
"tests": [
{
"file": "crates/batten/tests/renovate_config.rs",
"mutation": "drop the path-vs-inline distinction, so a config path is read as content"
}
]
}Acceptance
CLOUD-1278 An override's articulation dies with the container, so the forcing function is a toll with no audit trail and no reader
Why CLOUD-1051 replaced the override passwords with issued capabilities, and CLOUD-1120 made a spent admission actually suppress its finding. What neither did is give the articulation a reader.
That reading of rule 4 is wrong, and it is the defect. Rule 4 stops a gate republishing REPOSITORY content — a secret it scanned, a file it read, a subject line somebody typed. An articulation is none of those: it is the caller's own words, composed for the express purpose of being read by a reviewer. The stated intent for the mechanism was: increase the token cost of editing protected paths without preventing it, while deterministically producing an audit trail that lets a later reader diagnose what prose led to a bad decision. Half of that shipped. The half that shipped is the toll. The destination A commit message. It is durable by construction, travels with the change it justifies, needs no network to read, and is already in front of every reviewer and review bot per-commit. A pull request body loses on all three counts that matter: it is mutable after the fact, it is one blob for N commits so the per-write binding is lost, and writing it is Refinement — Ready Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Not in this issue A CLOUD-1125's head-binding problem is adjacent and NOT solved here: the block records Refs: CLOUD-1051, CLOUD-1120, CLOUD-1125, CLOUD-1050 CLOUD-1226 A registered `[[rule]] module` path is protected by derivation, and nothing says so: `protected` lists three globs, no `[[redirect]]` names the class, and the refusal tells you to use a Serena memory tool
Why Writing to a Measured 2026-08-31 on
So the discriminator is neither the directory nor the extension: it is being named by a The protection itself is right. A module is the policy authority as much as What is wrong is that it is unauditable and its remedy is somebody else'sThe remedy names a surface that cannot own the file. Because no There is no surface that owns a And a reader auditing Cost, measured rather than argued: it took four refusals and four probes in one session to establish what a reader should have got from one table, and each refusal was resolved by spending RETRACTED: "the verb is misattributed on a compound call" — this did not reproduceAn earlier revision claimed
The attribution is correct on every compound case tested. The original reading
RETRACTION: the "declared protection is inverted on Bash" measurement was wrong, and it was mineAn earlier revision of this row claimed that Re-measured 2026-08-31 on
The declared protection holds, on every verb tested. The discriminator is not The cause is CLOUD-1133's fix landing at one reader, and it has its own row now
That is now CLOUD-1236, Urgent, and it owns the fix. Not restated here — one What remains this row's, and it is realBoth of these reproduced cleanly:
Refinement — Ready (declare the class, give it a redirect, anchor the verb per segment) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found while repairing CLOUD-1222 and CLOUD-1156: four consecutive refusals over |
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (4)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (17)
💤 Files with no reviewable changes (2)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds subject-bound mediated admissions for protected writes. Admissions can be printed as self-verifying commit-message blocks and checked for committed or staged protected-path changes. Protected-path checks remain active during bypass, while spent matching admissions can allow mediated denials. The change also moves Renovate validation into a Batten command rule, adds a protected policy redirect, recognizes Merge Risk: 🟠 High · up to Protected-path writes now rely on spent admissions instead of the bypass password, but the admission reader can trust a forged spent record from the local store without validating its integrity or history. Anyone able to modify that store could authorize a protected repository mutation, so this authorization check should be hardened before merge. Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
76151b9 to
8e1b861
Compare
93a096d to
3823c43
Compare
6e84017 to
48f03aa
Compare
Shape 1 of CLOUD-1262, which that row names and which I filed instead of doing. The row says it plainly - "cheap, stops laps 3..n" - and then the session that wrote it shipped neither half. This is the half that was one edit away, next to the identical rule landed for `regal` an hour earlier. Renovate 44 reads `RENOVATE_CONFIG` as INLINE JSON5 config. `mise-tasks/renovate-config-validator.sh:32` uses that same name for the path seam its suite sets, so the validator is handed `/tmp/bats-run-.../renovate.json5` as config CONTENT and dies at `JSON5: invalid character 't' at 1:2`. WHY A RULE RATHER THAN THE COMMENT THAT ALREADY EXISTS. The collision was documented directly above `mise.toml`'s pin, in capitals, and recorded in ec2c9c1's commit message. The bump was still made and reverted twice: lap 1 44.50.3, verify refused, reverted in ec2c9c1 lap 2 44.41.0 via GitHub #494, verify refused identically, reverted Each lap cost a full cold verify to rediscover a settled decision, and lap 2's author had the comment on screen while editing the line under it. Prose is feedforward only; a rule is the mechanism. Renovate reads neither, so it re-proposes 44 indefinitely, and `auto-bot-land.yml` merges a bot bump on green CI with nobody in the loop - lap 3 is stopped today only by verify happening to be red for it, which is luck rather than a refusal. WHAT THIS IS NOT. It buys quiet, not currency. `npm:renovate` stays three majors stale on the tool whose whole job is dependency currency, and the actual fix is the port-and-retire CLOUD-1262 tracks: both shell paths deleted, the predicate landed as a policy module plus a compiled-binary tier, the seam renamed. The row comment says DELETE THIS ROW as part of that port, because a suppression left behind after its reason is gone is how a temporary staleness becomes permanent. `matchUpdateTypes` rather than `allowedVersions` for the reason 20d7e81 records: `cap-drift` pairs an allowedVersions rule with a Cargo.toml cap (CLOUD-593), and a `[tools]` entry has none, so the ceiling form reads as the half-lift that gate exists to catch. Refs: CLOUD-1262
…top being the only way through `protected-mutation` refused every write to a protected path and offered one exit: `BATTEN_HOOK_BYPASS=1`. That is a knowable string the guarded party can set, so it records nothing and stops nobody — it only prices honest work. This repository had already ruled on that exact shape for `V-FILED-OVER-OWN-DIFF`, in its own words: "the point of the admission mechanism is that the bare variable stops working." The mechanism to replace it has been landed since CLOUD-1051 and its doc comment is the whole design: an override that is a RECORD, not knowledge. `request` answers a class's declared precondition and issues a content-addressed admission; `spend` consumes it. The gate never adjudicates the reason - the predicate is "every declared question answered non-emptily", never "is this justification good", which would be the model verdict non-negotiable rule 3 forbids. The cost is thinking, not asking. WHY IT NEVER REACHED THIS CLASS, which took reading to find and is two gaps rather than one. First, `V-PROTECTED-MUTATION` declared two routes and neither was an `override` route, so `admission::questions_for` answered `None` and `batten override request` replied that the class "declares no `override` route, so it cannot be overridden". This adds `R-ARTICULATE-THE-WRITE`, whose precondition names the owning surface deliberately: the first question forces the asker to say why the route they were already given does not reach. That matters here because for the changes that need this - registering a rule, adding a redirect, retiring a gate onto a config row - `R-USE-THE-OWNING-SURFACE` names the very file being refused. Second, and the half that makes the first safe: nothing on the mediated path ever consulted the store. `filter_admitted` has dropped admitted findings on the TREE surface since CLOUD-1120; the hook had no twin, and `rules.rs` states the consequence outright - every native refusal is "simply not admissible, because there is no token an admission could bind". Shipping the route alone would have advertised an override, sent the caller through a real articulation, minted a real record, and still refused. That is precisely the class `verdict.rs` exists to kill: a refusal that names a remedy which does not work. `admit_mediated` is that twin. It binds the same five fields, so an admission cannot be harvested across gates, classes, subjects, trees or policy generations. It runs ONLY on a deny, which is what keeps it inside the invocation budget - a call nobody refuses reads no store, so `passthrough` stays below `noop`. It sits at the boundary rather than the deny site because `adjudicate` is pure by contract and reading a store is I/O. `Refusal` gains the subject an admission binds to - the first path-bearing one, which is already the finding's own pointer, so this is the same choice that surface makes rather than a second one. Carried rather than re-derived, because a boundary recomputing "which path was refused" would be a second authority over a question the deny site already answered. Not serialized, so `-J` output is byte-identical. WHAT THIS COMMIT DELIBERATELY DOES NOT DO. `BATTEN_HOOK_BYPASS` still answers for this class. Removing it is the half that makes the bound real, and it lands next - after a test proves a spent admission actually opens the gate. Pulling the old exit before the new one is demonstrated would leave the repository with no way through if this path has a bug, which is a worse failure than the one being fixed. Refs: CLOUD-1262
… binary `9eaa5e74` declared `R-ARTICULATE-THE-WRITE` and wired `admit_mediated`, and until this suite existed both were a promise: the refusal advertised an override and nothing demonstrated that spending one reached the gate. A route that names a remedy which does not work is the defect `verdict.rs` exists to kill, so the route and its proof belong in the same change set. Four cases in a new `crates/batten/tests/mediated_admission.rs`, over the compiled binary rather than `with input as`: the question is whether the ENGINE honours the record, which a fabricated input cannot answer. a_write_to_a_protected_path_is_refused the premise a_spent_admission_admits_the_write_it_was_taken_for the whole point an_issued_but_unspent_admission_does_not_admit only Spent admits an_admission_for_another_subject_does_not_admit_this_one the binding A FIXTURE RATHER THAN `mediated_verbs.rs`, which adjudicates against the live repository root. These cases must WRITE an admission, and the store lives under `$GIT_DIR` — so hosting them there would deposit records in the developer's own repository and bind its real HEAD. THE PREMISE CASE EARNED ITS PLACE ON THE FIRST RUN, which is the argument for writing it rather than a note about diligence. All four were red; it failed `Some(1)` vs `Some(2)`, and the `1` is a usage error rather than a verdict — the fixture's config was invalid, so nothing was adjudicating at all. Without that case the admission tests would have gone green the moment the config parsed, against a gate that never fired, and the suite would have proved nothing while reading as proof. THE `[[verb]]` ROW IN THE FIXTURE DOES NOT MATCH THE CALL AND IS STILL REQUIRED, which is worth stating because it reads as a mistake. `Policy::is_empty` is `shapes ∧ bundles ∧ (verbs ∨ protected)`, and it short-circuits `adjudicate` before any gate runs — so a repository declaring protected paths and no verb rows cannot refuse a write tool, even though `protected_tool_write` needs no matching row and treats a verb miss as "no verb-level remedy" rather than as grounds to allow. The row is what makes the policy adjudicable; it is not what selects the call. Recorded in the fixture's own comment so the next reader does not delete it as dead weight. That asymmetry looks like a real defect rather than a design and is filed separately rather than widened into here. `issued_but_unspent` is the economy `admission.rs` names: a mint that suppressed on its own would restore the variable it replaced — hold the name, pay nothing, override forever. Articulating is not overriding until it is spent. With this green, removing `BATTEN_HOOK_BYPASS` for this class is next, and it is now safe: the replacement is demonstrated rather than assumed. Refs: CLOUD-1262
The password stops working. `BATTEN_HOOK_BYPASS` was the only way through a `V-PROTECTED-MUTATION` refusal, which made that refusal a knowable string the guarded party can set — recording nothing, stopping nobody. §8's property, "an agent's context can never influence the rules it is judged by", was already false because the agent could set the variable. This is only safe because the previous two commits made the replacement real: the class declares `R-ARTICULATE-THE-WRITE`, and `mediated_admission.rs` proves over the compiled binary that a spent admission opens the same write. Taking the variable away is therefore a repair rather than a wall. Doing these in the other order, or together, would have left a window with no way through at all. THE FIRST ATTEMPT WAS A DEAD CHANGE, and the way that surfaced is the reason the binary tier exists. The narrowing was written inside `adjudicate` and its unit cases went green — while the compiled binary still ALLOWED the write. The boundary hands `adjudicate` a policy declaring nothing when the hatch is set, so `policy.is_empty()` returned before the new branch could run. A unit case cannot see that wiring by construction. `the_bypass_hatch_does_not_open_the_protected_ gate_over_the_binary` is what caught it, and without it this commit would have claimed the password was retired while the password still worked. So the real edit is at the boundary: an adjudicable call loads its policy whether or not the hatch is set. `adjudicate` then runs the two `protected_write` stages under the hatch and allows everything else, which closes both surfaces — covering the write tool alone would have left `tee batten.toml` bypassable while `Write` was not. AN INVARIANT IS RETIRED RATHER THAN ERODED. `BYPASS_ENV`'s doc said "a bypassed call must never pay a config read". That cannot survive: deciding whether a path is protected needs the `protected` and `[[verb]]` tables, which ARE the config. A bypassed adjudicable call now pays one load — the `noop`-to-`check` difference in `perf`, ~0.7 ms against a 100 ms budget. A call with nothing to adjudicate still skips it, and that is the arm the hot path rides. Both doc comments say so now instead of asserting a property the code no longer has. TWO EXISTING ASSERTIONS CHANGED, both deliberately, both documented at the site: `the_protected_gate_honours_the_bypass_hatch` is inverted and renamed. It was correct for its whole life — while the variable was the only way through, honouring it was the difference between a gate and a wall. Its complement, `the_bypass_hatch_still_reaches_an_explicit_row`, is new and is the discriminator: without it the inverted case would pass equally well had the hatch been deleted outright, and a reader could not tell a scoped exemption from a removal. `bypass_scrub::the_hatch_is_load_bearing` is re-pointed at a `shape` row. Its subject is CLOUD-1227's environment scrub, not the protected gate; it used a protected refusal only as a convenient way to observe that the hatch was live. Observing it through the one gate the hatch cannot open would assert the opposite of this contract, and relaxing its assertion instead would have destroyed what the file is for — a hatch that disarms nothing needs no scrub. `hook_honours_the_bypass_hatch` is untouched and still passes: it drives a `shape` row, which is the rest of the mediated surface and is what the hatch still answers for. 3471 passed, 0 failed. Refs: CLOUD-1262
Found by becoming the caller. Attempting the next edit in this branch — adding a `[[redirect]]` row to `batten.toml` — the gate refused with a message ending "Bypass with BATTEN_HOOK_BYPASS=1", the variable 9cac169 had just made inoperative for exactly that class. So that commit created a fresh instance of the defect this whole change exists to remove: a refusal naming a remedy that does nothing. `crate::verdict`'s header names the class outright — "a refusal could name no remedy, name a task that does not exist, offer an override with no precondition" — and reading the message as its author would not have caught it. Hitting it as somebody trying to get work done did, immediately. TWO HALVES, and the second is the one that matters. The hatch sentence is omitted for `V-PROTECTED-MUTATION`, keyed on the same fact the boundary decides on rather than an exempt-list that would drift out of agreement with it. Omission alone would have been a worse refusal than the wrong one. `render`'s fix comes from `first_command_route`, which by construction cannot be the override — so the way through would have been declared, honoured, and undiscoverable from the one place a caller is looking. The refusal now names the route as the COMMAND that takes it, composed from the refusal's own rule, class and subject, because those three fields ARE the binding `admission::admitted` checks: a caller who runs the line back gets an admission for the situation they are in and cannot be handed one for a different situation. That is the second thing `Refusal`'s subject field (9eaa5e7) bought. THE SHARED PROJECTION SPLIT, and the test says so rather than being relaxed. `both_deny_paths_render_one_shape_and_neither_can_drop_the_fix_clause` required the hatch on every deny, which was right while the hatch reached every row. It now asserts what every deny still owes — `Refused by` and `Fix:` — and then asserts both directions separately: an explicit `[[rule]]` row still advertises the hatch, and a protected deny must NOT advertise it AND MUST name `batten override request`. That second conjunct is the point. Asserting only the absence would pass a refusal that offers nothing at all, which is strictly worse than the wrong remedy it replaced — and an assertion loosened to go green is indistinguishable later from one that was always weak. Third existing assertion this change has moved, all for one reason: the hatch used to be universal and is not any more. Each was inverted or split rather than weakened, with the reversal recorded at the site. 3471 passed, 0 failed. Refs: CLOUD-1262
… stops being told to use a memory tool CLOUD-1226. Every enabled module and bundle root is a protected path, derived from the rule table rather than listed in `protected` (CLOUD-763/833). Those paths match no glob in the `[[redirect]]` table, so `protected_refusal`'s three tiers fell through to tier two — the verb's own `redirect` — and for a write tool that text ends "for a memory that is the Serena tool `write_memory`". Editing `policy/shell-retirement.rego` was therefore answered with advice about `edit_memory`, on a file that is not a memory and that no Serena tool can write. Measured this session: that refusal was read as the engine being broken, and it cost real time before the mechanism was understood. THE VERB ROWS ARE NOT THE DEFECT, and repairing them would have been the wrong fix — the tempting one, since the misdirecting string is right there. This file's own note above the table already states the rule: per-path beats per-verb only where the path fact dominates, and memories are deliberately absent BECAUSE their remedy varies by action (`write_memory` / `edit_memory` / `rename_memory` / `delete_memory`). Rewriting `tee`'s text to stop naming memories would have broken the case the fallback exists for. What actually changed is that a second class started reaching the same fallback, and a per-verb string naming one path class is only ever right when the path happens to be that class. `policy/**` RATHER THAN THE DERIVED SET, because this table is globs over paths and the derived set is a property of the loaded rules — a row cannot name "whatever is registered". The glob is wider than the derived set by exactly the unregistered modules, which is the safe direction: an unregistered module is not protected, so the row is unreachable for one and costs nothing. HOW THIS EDIT WAS MADE, which is the point of the three commits before it. `batten.toml` is protected, and as of 9cac169 no variable opens that gate. The edit went through the declared route: an admission requested against `V-PROTECTED-MUTATION`, answered, and spent. admission fa60f5bb6cda25de4c7b28156ba7bb1b88b0841002086780f466545e5336ad8a precondition R-USE-THE-OWNING-SURFACE names batten.toml and the change IS to batten.toml, so the surface this class points at is the subject being refused and cannot express it lost CLOUD-1226 stays open: a registered module refusal keeps telling authors to fix a .rego with the Serena edit_memory tool, which cost this session real time rejected-route R-RESTORE-IT is git restore, which undoes an edit rather than making one, and there is nothing to restore because the redirect row does not exist yet The address is a hash of those answers, so editing them here invalidates it and reusing them reproduces a spent address rather than a fresh one. That is the first real spend outside a fixture, and it is the property this whole change set exists for: not a password anyone can know, a record somebody had to write. Noted rather than claimed: re-presenting that admission now reports `unbound` rather than `spent`, because the binding carries the config epoch and this very edit moved it. An admission for a config change is therefore single-use by construction. The spent-cannot-be-respent property is `admission.rs`'s own suite, not something this commit demonstrated. config-lint 0 smells; 3471 passed, 0 failed. Refs: CLOUD-1226 Admits: 094f50b0b0f9c32bb159d4d3c0a5123c273ac021cd2841302a425015fb5aa269 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 04cb4e5 Admits-epoch: 104edb892c2db8711f1a16aa692c1ee87d41597d70af74d823174026e52a5fdc Admits-author: alec@wenzowski.com Admits-prev: d2f7d27a7ed140e441c9800b2f88e2c9bbefc4e09cee6c3f4dfcbf3331e3167d Admits-answer-lost: Nothing is lost by writing it here. The alternative routes do not reach a config row at all: there is no other file the engine consults for one. Admits-answer-precondition: `batten.toml` is the one authority the engine reads for rules, so a `[[rule]]`, `[[redirect]]` or `protected` row cannot be added anywhere else; the surface this class names is the file itself, and the change is one a reviewer sees in the diff it lands in. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular for this subject — `batten.toml` IS the owning surface for a rule row. R-RESTORE-IT would revert the change this commit exists to make.
…rride leaves a trail The override route made a protected write ADMISSIBLE and produced nothing anyone could read. `admission::store_dir` resolves under the OS data directory — here a container the platform reclaims — so the reasoning an override cost was legible only inside the session that wrote it, and only until that session ended. There was no read verb either: `override` offered `request` and `spend` and nothing that could show a record back. A forcing function whose product nobody can read is a toll, not an audit trail. This is the half that makes it one. THE BLOCK, AND WHY IT VERIFIES WITH THE STORE DELETED. `admission::block` renders a record as `Admits:` plus one line per binding field and one per answer. EVERY binding field is spelled out, so `Articulation::recomputes` is a pure function of the message: a runner that has never seen the store decides identically, which is what makes this a CI tier rather than a local one. A block carrying only the address would be a pointer into a store that has already been reclaimed — the exact failure being fixed. One long line per answer and no folding: unfolding is lossy the moment an answer holds a run of spaces, and that shows up as an address that does not recompute over text nobody edited — a false tamper report, strictly worse than a long line. TWO FINDINGS THAT MEAN OPPOSITE THINGS. `admits` is a protected path written with no block claiming it. `admits-tampered` is a block that claims the path and does not hash to the address it names. Rolling both into "missing" would let a doctored articulation read as an honest omission, which is the one failure a content-addressed record exists to expose. `commit::Finding` gains the path it is about. That is a pointer, not a payload — §6 names `path:line` outright — and without it an author is told a commit lacks an articulation and left to work out which of its protected paths. `skip_serializing_if` keeps `-J` over a subject-clause run byte-identical. RULE 4 WAS BACKWARDS AT `spend`'s OUTPUT SITE, and the assertion pinning it is inverted here rather than worked around. The comment read "POINTER, NEVER THE ANSWERS (rule 4) … the reasoning the author typed stays in the store". Rule 4 stops a gate republishing REPOSITORY content — a secret it scanned, a file it read. An articulation is the caller's own words, composed to be read by a reviewer; `admission.rs`'s header already says a record is "safe to print, log, quote in a commit and leave in a transcript", and `refusal.rs` calls it "rule 4's deliberate inversion". Honouring the letter of a rule that was not about them cost the design its entire point. `spend` now prints the block after its pointer line, which stays first and unchanged. `git::writes_in_range` is the range reader: per-commit message plus the paths it moved under the protected globs, compared by BLOB ID inside the tree walk, so a commit touching nothing protected costs two tree reads and no blob reads. A root commit compares against the empty tree — otherwise the first commit of a repository is the one place a protected path can be introduced unarticulated. THE KEY IN AN EARLIER DRAFT OF THIS COMMIT WAS WRONG. It cited CLOUD-1264, which is a real but unrelated fleet-dispatch record; the row this work belongs to is CLOUD-1278, filed for it. Corrected in every source comment and here. `crates/batten/tests/commit_admission.rs` is the compiled-binary tier, seven cases. `an_unarticulated_protected_write_is_refused` is the premise: a fixture whose protected glob selected nothing yields no paths, so every admitting case would pass over a clause that never fired. `the_clause_needs_no_store_to_decide` removes the store and asserts the block still verifies. `mediated_admission.rs` gains the test-target lint header it landed without — clippy is in the slow profile, so pre-commit did not see it. Refs: CLOUD-1278, CLOUD-1051, CLOUD-1050
… a memory tool CLOUD-1226 §7's obligation, which the redirect row landed without. That commit was 28 lines of `batten.toml` and no test, so the row was served and not completed — and `closing-key-check` is what said so, refusing to let the PR close some of the keys its commits served and strand this one. THE MIRROR IS WHAT MAKES IT DISCRIMINATE. Asserting only that a module's refusal omits `write_memory` is satisfied by a build that stopped naming the Serena tools anywhere, which would break the class they were written for. So the second half asserts a memory write still gets `edit_memory`. Both directions, or neither means anything (CLOUD-418). Shown able to fail: with the `policy/**` `[[redirect]]` row's glob pointed elsewhere, the case is red at the `write_memory` assertion — the derived class falls back through `protected_refusal`'s tier two to the verb's own redirect, which is the defect CLOUD-1226 records. It lives in `mediated_verbs.rs` because that suite adjudicates against the LIVE repository root, and "what does the committed redirect table decide" is exactly its question. A fixture would assert about a table it wrote itself. Refs: CLOUD-1226, CLOUD-1050, CLOUD-418
48f03aa to
a673667
Compare
|
❌ The last analysis has failed. |
|
/fast-forward |
What this is
protected-mutation's only way through wasBATTEN_HOOK_BYPASS=1— a knowable string the guarded party can set, recording nothing. Read the refusal and you hold the bypass forever, for every subject, in every session. The replacement already existed (batten override request/spend, CLOUD-1051 — "an override that is a record, not knowledge") and had never been wired to this class.This PR wires it, removes the hatch for that one class, and gives the articulation somewhere durable to land.
The three pieces
1. A mediated refusal becomes admissible.
V-PROTECTED-MUTATIONdeclaresR-ARTICULATE-THE-WRITE, anoverrideroute with a precondition.Refusalnow carries the subject an admission binds to.admit_mediatedis the mediated twin offilter_admitted: on a deny, it looks for a spent admission over all five binding fields and allows if one covers it.The route alone would have been harmful — advertising an override, minting a real record, and still refusing. That is the defect class
verdict.rsexists to kill, so the engine half is the deliverable and the route is the advertisement.2. The hatch no longer opens this class. The protected-path gate adjudicates ahead of the
bypass || is_emptyshort-circuit, the wayStopalready does. Every other mediated row keeps its hatch. Existing cases asserting the hatch silences this class are inverted — they are the change, not collateral.This was caught as a dead gate first: the unit tests were green and the binary still allowed, because the boundary handed
adjudicatean empty policy under bypass. Only the binary-level tier saw it.3. The articulation gets a reader (CLOUD-1278). The record lives in a container-scoped store, so the reasoning an override cost was legible only inside the session that wrote it.
override spendnow prints anAdmits:block carrying every binding field, andbatten commit checkrefuses a commit that wrote a protected path without one —admitswhen absent,admits-tamperedwhen a block does not hash to the address it names.Because the block spells out every field,
recomputesis a pure function of the message: the clause decides identically with the store deleted, which is what makes it a CI tier rather than a local one.spend's output site previously invoked rule 4 to keep the answers in the store. Rule 4 stops a gate republishing repository content; an articulation is the caller's own words, written to be read by a reviewer —admission.rsalready says a record is "safe to print, log, quote in a commit". That assertion is inverted here with the argument at the site.The PR is its own first subject
A commit here wrote
batten.toml, so it was replayed to carry an articulation bound to its own parent.batten commit check origin/main..HEADis exit 0 — reached by articulating, not by exempting.What was dropped, and why
This PR originally also retired
renovate-config-validatorand bumpednpm:renovate(CLOUD-1262).claim-race-checkrefused it: #797 already claims that key, is open and mergeable, and is not stale. Racing it is the defect CLOUD-230 measures, so the two retirement commits were dropped and #797 owns the row.Dropping them also removed a fourth
has_policy_surfacearm I had added on a false premise — that a "spawn a tool, read its status" predicate has no Rego spelling. It does:input.tree["tool-verdict"](CLOUD-1171) is declared onmaintoday, and #797 is building the producer that fills it. The mechanism was not missing; only its producer was. Detail and one carried-over finding are on CLOUD-1262.650899fastill serves CLOUD-1262 — it is therenovate.json5packageRulethat refuses renovate's own major line, which that row names as shape 1 of two and says outright is "not the fix and must not be mistaken for one". It buys quiet, not currency. So this PR declines to close that key rather than moving the row a column ahead of the port that #797 is landing.Notes for review
Admits-*blocks are the audit trail. They are the caller's own words and are the thing to read adversarially.commit::Findinggains an optional path. That is a pointer, which §6 names as an allowed shape; without it an author is told a commit lacks an articulation and left to guess which protected path..regomodule was being told to use a Serena memory tool, because the derived protected class matched no[[redirect]]glob and fell through to the verb's own remedy. Its §7 test asserts both directions — a module getspolicy-test, a memory still getsedit_memory— since the first alone is satisfied by a build that stopped naming the Serena tools at all.Closes CLOUD-1278
Closes CLOUD-1226
DO-NOT-CLOSE CLOUD-1262