Skip to content

fix(lease): a present-but-empty schema field is refused, not read as the oldest - #948

Merged
wenzowski merged 2 commits into
mainfrom
claude/batten-pr-934-takeover-mtdtnq
Sep 11, 2026
Merged

fix(lease): a present-but-empty schema field is refused, not read as the oldest#948
wenzowski merged 2 commits into
mainfrom
claude/batten-pr-934-takeover-mtdtnq

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Body::schema exists so a reader can refuse a lease body it does not speak. An
empty value defeated it.

parse_body splits each line on ": ". A line written schema: — the field
present, the value empty — has no ": ", so it never reached the arm that
parses the major. It fell through the empty-value block's _ => {}, left
schema at None, and the reading below turns None into BODY_SCHEMA. A
body whose writer disagrees with us about what the field is was parsed
loosely and acted on: the exact failure the major was added to stop.

None => BODY_SCHEMA is right for a body written before the field existed.
It is wrong for one carrying the field and saying nothing in it, and once the
line is dropped on the floor the two are indistinguishable.

Why the existing case missed it

an_unreadable_major_is_refused_rather_than_treated_as_the_oldest asserts this
property and passed throughout. Its fixture is schema: tomorrow, which does
contain ": ", so it reaches the match arm, fails parse::<u32>(), and takes
the refusal path. Two spellings of one case took different branches and the
suite pinned the branch that already worked.

The fix

One arm — "schema" => schema = Some(None) — routing the empty value to the
same refusal an unparseable one takes. No new state, no second matcher.

Evidence

//MUTANT empty-schema-reads-as-oldest reverts the arm. Measured under it: the
declared case a_body_carrying_an_empty_major_is_refused dies, and the two
anti-vacuity twins stay green — the absent-field hinge
(a_body_with_no_major_at_all_still_reads_as_the_oldest, without which the fix
is satisfied by refusing everything and stopping the fleet on deploy) and a
good schema: 1 body. So the refusal is keyed to the emptiness, not to the
field being present.

Full suite green (mise run test, exit 0).

Blast radius

Body::render always writes a number, so nothing in this crate emits a bare
schema:. The exposure is a future writer, a hand-edited ref, or a partial
write. It is worth closing because it is a fail-open arm inside a mechanism
whose entire purpose is to fail closed.

Also here

One unrelated commit: mcp__github__pull_request_read added to
permissions.allow. Read-only, and the only candidate from a scan of this
session's tool calls that was not already covered by an existing entry.

Still owed from #934

  • lease::cooling has no writer — nothing records poisoned_at on a red CI
    wait, so the cooldown predicate is landed but unreachable.
  • lease::notice has no sender verb — the stand-down field, mint and reader
    are landed; no verb writes one.

Closes CLOUD-1792

DO-NOT-CLOSE CLOUD-1426

That row asks whether a committed permissions.allow entry reaches the remote
harness at all — its own measurement shows mcp__serena__* granted in this file
and prompting anyway. This commit writes one more entry into the same file; it
does not answer the question, and this session gave it fresh evidence rather
than a fix (mcp__Linear__save_issue prompted here despite the connector being
set to always-allow). The row stays open.

https://claude.ai/code/session_014zmrMLGEsPxiTyFRq28uXX

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 1c9982ff-6603-4ec6-b059-fc407f9004cc

📥 Commits

Reviewing files that changed from the base of the PR and between 45ca0a7 and 728aa84.

📒 Files selected for processing (2)
  • .claude/settings.json
  • crates/batten/src/lease.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The lease parser now refuses bodies with an empty schema: value. It still accepts bodies without a schema and bodies with schema major 1. Tests cover all three cases. Claude settings also allow the mcp__github__pull_request_read tool.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 728aa

The lease parser now rejects malformed empty schema fields while preserving supported legacy inputs, and the requested pull-request read permission is explicitly scoped. No actionable merge risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the lease parser change, test coverage, mutation testing, permissions update, and remaining work.
Title check ✅ Passed The title is concise, specific, and accurately identifies the main lease parsing change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 1 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/batten-pr-934-takeover-mtdtnq

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…the oldest

`parse_body` splits each line on `": "`, so a line written `schema:` — the
field present, the value empty — never reaches the arm that parses the major.
It fell through the empty-value block's `_ => {}`, left `schema` at `None`,
and the reading below turns `None` into `BODY_SCHEMA`. A body whose writer
disagrees with us about what the field IS was parsed loosely and acted on,
which is the exact failure the major was added to stop.

`None => BODY_SCHEMA` is right for a body written BEFORE the field existed. It
is wrong for one carrying the field and saying nothing in it, and the two are
indistinguishable once the line is dropped on the floor.

The existing case asserts this property and passed throughout: its fixture is
`schema: tomorrow`, which DOES contain `": "`, reaches the match arm, fails
`parse::<u32>()`, and takes the refusal path. Two spellings of one case took
different branches and the suite pinned the branch that already worked.

`Body::render` always writes a number, so nothing in this crate emits a bare
`schema:`; the exposure is a future writer, a hand-edited ref, or a partial
write. It is worth closing because it is a fail-open arm inside a mechanism
whose entire purpose is to fail closed.

`//MUTANT empty-schema-reads-as-oldest` reverts the new arm. Measured: the
declared case dies under it and the two twins — the absent-field hinge and a
good `schema: 1` body — stay green, so the refusal is keyed to the emptiness
rather than to the field being present.

Closes CLOUD-1792
`mcp__github__pull_request_read` is the one MCP call this session made
repeatedly (8 times) that was neither already in `permissions.allow` nor
covered by a broader entry: it is how a land reads CI and review state off a
PR, and it prompted on every one.

It is read-only, so it widens nothing a land could not already observe. No
other candidate survived the scan — every Bash hit was already subsumed by an
existing wildcard or auto-allowed by the harness, the whole `mcp__serena`
server is allowed, and Linear is granted at the connector.

Nothing was removed, and nothing was added to `deny` or `ask`.

Whether this grant actually reaches the remote harness is CLOUD-1426's
question, not this commit's answer — the entry is written where the repo keeps
its grants and that row owns the layer.

Refs: CLOUD-1426
@wenzowski
wenzowski force-pushed the claude/batten-pr-934-takeover-mtdtnq branch from 840251c to 728aa84 Compare September 11, 2026 21:31
@wenzowski
wenzowski marked this pull request as ready for review September 11, 2026 21:50
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 728aa84 into main Sep 11, 2026
21 checks passed
@wenzowski
wenzowski deleted the claude/batten-pr-934-takeover-mtdtnq branch September 11, 2026 22:03
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