Repository navigation
Reject leases the gate never issued - #7
Merged
Merged
Conversation
Interlock.permit() trusted whatever Lease object it was handed, so a hand-built Lease with a far-future expires_at passed the gate for any resource. LeaseManager now keeps its own record of every lease it grants, and that record decides expiry and revocation. permit() raises the new LeaseNotIssued for leases it did not issue or whose identity was edited, and logs the denial with reason lease_not_issued.
FetchDoge
added a commit
that referenced
this pull request
Oct 8, 2026
Resolve the quickstart conflict with #7: keep LeaseNotIssued in the gate order, keep the admission-only caveat, import the errors from the public interlock package, and use a 3.11+ python3 for the venv. Docs only.
FetchDoge
added a commit
that referenced
this pull request
Oct 8, 2026
Resolve README and CHANGELOG conflicts with #4-#7: keep main's restructured README and add the demonstrator line in house style; fold the demonstrator entry into the Unreleased Added section. Re-ran scripts/demo_90s.py on the merged tree: exit 0 on 6 runs, output identical to demo/TRANSCRIPT.md except the random lease id, so the transcript is unchanged.
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
Interlock.permit()now refuses any lease it didn't issue.LeaseManagerkeeps its own record of every lease it grants, and that record, not the caller'sLeaseobject, decides whether a lease has expired or been revoked. A new exported exception,LeaseNotIssued(InterlockError), signals the refusal.Why
permit()checked only the fields on theLeaseobject it was handed. The manager kept no record of what it had issued, so anyone could build a lease and pass the gate for any resource. Repro onmain:On
main, pushinglease.expires_atforward also extended authority, and settinglease.revoked = Falseafterrevoke()restored it.Guarantee
permit()checks the lease in this order: e-stop clear, then issued by this facade'sLeaseManagerwith resource, holder, TTL and grant time unchanged, then unrevoked, then unexpired, then no gating fault.InterlockorLeaseManager, or a lease whose resource, holder, TTL or grant time was edited after issue raisesLeaseNotIssued. The ledger logs adenialsrecord with reasonlease_not_issued, and the action body never runs.expires_ator clearingrevokedon the object grants nothing.keepalive()raisesLeaseNotIssuedfor unissued leases. It still renews issued leases, and it updates both the record and the object.is_live()returnsFalsefor unissued leases. New helpersissued()andis_revoked()are available.grant()thenpermit()orkeepalive(), behaves as before. All 59 existing tests pass unchanged.Not promised: the record lives in process memory and only covers leases this process issued, the same as the e-stop and fault bus. It doesn't defend against code that edits the manager's private
_issuedmap or sidestepspermit()entirely. The record keeps every issued lease, so memory grows by one small entry per grant.Failure behavior
Refusals raise
LeaseNotIssued(anInterlockError) and write a ledger denial, so an operator seesreason: lease_not_issued. Code that used to pass a lease from oneInterlockto another'spermit()now gets refused. That's the intended break, and the CHANGELOG notes it.Tests
New file
tests/test_lease_issuance.py(9 tests: forged lease, a lease from another instance, a retargeted lease, editedexpires_at, a clearedrevokedflag, keepalive on an unissued lease, keepalive still renewing an issued one,is_live()on an unissued lease, and the export).On
main(cc6b812), with only the new test file copied in:With this fix:
pytest -qpasses (68; it was 59)ruff check src testsandruff format --check src testspassCHANGELOG (Unreleased → Fixed and Added), plus the README guarantee list, quickstart comment, how-it-fails table and test count,
docs/guides/quickstart.md,docs/concepts/leases.md, and the matching line inCHARTER.mdare updated.