Release v1.1.1 — survive a never-written DAR collection, and stop stranding the sentinel - #11
Merged
Merged
Conversation
…anding the sentinel A lakehouse whose OneLake security has never been written answers every dataAccessRoles read with the implicit DefaultReader under a freshly minted id, while the collection ETag and the role's content stay identical. ControlBoundary.snapshot_from hashed the whole role, id included, so the two reads begin() takes a second apart never matched, and every first-ever generate on a new lakehouse was refused with "DAR state changed after the approved snapshot" — deterministically, and only there, because a collection apply has written once carries stable ids. Observed on a customer estate's first pipeline run on 2026-09-02. That refusal then stranded its own marker: begin() had created the sentinel and raised before any lease existed, so release_unwritten_leases() had nothing to hand back, and the next run met "control-data incident sentinel already exists" for an incident nobody had. clear_incident() performs the same two-read compare, so it was blocked too. - snapshot_from: the digest covers each role's content (name, rules, members) and excludes the server-assigned id and per-role etag; the collection ETag compared beside it still catches a real write between the reads. .roles keeps the full payload. - begin(): a refusal at the post-creation re-read removes the sentinel this call created before re-raising; a marker inherited from an outer lease is left alone. - run_mode: unwritten leases are released on the error path as well as on blocked; the unwind still stops at the first lease that authorized a write. - docs/control-data-security.md says what the snapshot compares and ignores. - tests: a never-written-collection fake, the content-change and cleanup branches, and the error-path release; the immutability test no longer pins the stranded marker as expected behaviour. Patch release: no public API or parameter changes; the warnings a caller sees are unchanged.
kengio
force-pushed
the
fix/fresh-collection-boundary
branch
from
September 3, 2026 08:02
ea4964c to
4f4ebfb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
A lakehouse whose OneLake security has never been written answers every
dataAccessRolesread with the implicitDefaultReaderunder a freshly mintedid, while the collection ETag and the role's content stay identical.ControlBoundary.snapshot_fromhashed the whole role,idincluded, so the two readsbegin()takes a second apart never matched — and every first-evergenerateon a new lakehouse was refused with:Deterministically, and only there: a collection
applyhas written once carries stable ids, which is why estates that had already deployed never saw it. Observed on a customer estate's first pipeline run on 2026-09-02 (three consecutive reads, three ids, one ETag).The refusal then stranded its own marker.
begin()creates the sentinel and only then re-reads the collection; it raised before any lease existed, sorelease_unwritten_leases()had nothing to hand back. The next run metcontrol-data incident sentinel already existsfor an incident nobody had — andclear_incident()could not clear it, because it performs the same two-read compare.What changes
ControlBoundary.snapshot_fromroles_digestcovers each role's content (name, rules, members) and excludes the server-assignedidand per-roleetag. The collection ETag compared beside it still catches a real write between the reads..roleskeeps the full payload.ControlBoundary.beginrun_modeerrorpath as well as onblocked. The unwind still stops at the first lease that authorized a write.docs/control-data-security.md__version__Security consequence
The gate is not weakened. What a role grants is still fingerprinted byte for byte, and a concurrent write in the read→write window still rotates the collection ETag and fails the compare. What no longer counts as a change is the service re-minting its own bookkeeping ids. The sentinel's incident semantics are untouched: a lease that authorized a write keeps its marker on every path (
test_release_unwritten_refuses_any_lease_that_reached_a_write,test_release_stops_at_the_first_lease_that_wrote).Tests
test_snapshot_is_immutable_and_reconfirmation_does_not_refresh_after_changeasserted the stranded marker as expected behaviour; it now asserts the refusal alone.pytest --cov: 100 % branch coverage holds.scripts/lint.shandscripts/check_public_release.py tree .pass.Release steps after merge
Tag with the sanitized maintainer identity per CONTRIBUTING (
git tag -a v1.1.1 -m 'release: OLAF v1.1.1', reading the identity offv1.1.0); this PR does not create the tag.