fix(lease-broker): no lease held is a no-op success, not a denied transition (#1339)
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #1339.
This commit is contained in:
2026-08-20 23:40:09 +00:00
parent af43a7a63e
commit 6306914965
4 changed files with 141 additions and 4 deletions
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""D29 contracts: no lease is a no-op success; half-provisioned still fails closed."""
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
REVOKE_PATH = TOOLS / "revoke-lease.py"
_spec = importlib.util.spec_from_file_location("revoke_lease", REVOKE_PATH)
assert _spec and _spec.loader
revoke_lease = importlib.util.module_from_spec(_spec)
import sys as _sys
_sys.path.insert(0, str(TOOLS))
_spec.loader.exec_module(revoke_lease)
ARGV = ["--runtime", "claude", "--reason", "pre-compact"]
VALID_SESSION = "a" * 64
def _explode(*_args, **_kwargs):
raise AssertionError("broker must not be contacted when no lease is held")
class RevokeWithoutLease(unittest.TestCase):
def test_no_lease_variables_is_a_noop_success(self) -> None:
"""The D29 case: bare-launched session, nothing to revoke, must not deny."""
self.assertEqual(
revoke_lease.main(ARGV, environ={}, request=_explode),
0,
)
def test_no_lease_does_not_contact_the_broker(self) -> None:
"""A no-op must be vacuous: no socket, no generation bump, no transport."""
revoke_lease.main(ARGV, environ={"HOME": "/nonexistent"}, request=_explode)
def test_socket_without_session_still_fails_closed(self) -> None:
"""Half-provisioned is misconfiguration, not absence. Fail-closed stands."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/tmp/nonexistent.sock"},
request=_explode,
),
2,
)
def test_session_without_socket_still_fails_closed(self) -> None:
"""The mirror case, so the guard cannot be satisfied by either half alone."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={"MOSAIC_LEASE_SESSION_ID": VALID_SESSION},
request=_explode,
),
2,
)
def test_empty_string_counts_as_absent(self) -> None:
"""An exported-but-empty variable is not a lease."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={
"MOSAIC_LEASE_BROKER_SOCKET": "",
"MOSAIC_LEASE_SESSION_ID": "",
},
request=_explode,
),
0,
)
if __name__ == "__main__":
unittest.main()
@@ -337,7 +337,13 @@ class ExecutableEntrypointTest(unittest.TestCase):
runpy.run_path(str(TOOLS_DIR / "launch-runtime.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 64)
def test_revoker_entrypoint_denies_when_identity_environment_is_absent(self) -> None:
def test_revoker_entrypoint_noops_when_identity_environment_is_absent(self) -> None:
# D29 supersession. This assertion previously pinned rc=2. Absent identity
# means no lease was ever held, so there is nothing to revoke and the correct
# result is no-op success. The old pin was written in e4d7d45 (WI-3), the same
# commit that shipped launch-runtime.py's lease-var provisioning, on the
# assumption that an envless revoker was unreachable. D29 falsified that in
# production. Behavioural pins live in src/lease-broker/revoke_noop_unittest.py.
with patch.object(
sys,
"argv",
@@ -352,9 +358,42 @@ class ExecutableEntrypointTest(unittest.TestCase):
io.StringIO()
), self.assertRaises(SystemExit) as raised:
runpy.run_path(str(TOOLS_DIR / "revoke-lease.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 2)
self.assertEqual(raised.exception.code, 0)
def test_revoker_entrypoint_denies_when_identity_environment_is_half_provisioned(
self,
) -> None:
# The no-op above is reachable ONLY when identity is TOTALLY absent. A
# half-provisioned environment is a machinery-present failure and must still
# fail closed. main() already pins this; the entrypoint did not, and the
# entrypoint is what the runtime extension actually spawns.
half_provisioned = (
{"MOSAIC_LEASE_BROKER_SOCKET": "/run/test/broker.sock"},
{"MOSAIC_LEASE_SESSION_ID": "d" * 64},
)
for environment in half_provisioned:
with self.subTest(environment=environment), patch.object(
sys,
"argv",
[
str(TOOLS_DIR / "revoke-lease.py"),
"--runtime",
"claude",
"--reason",
"pre-compact",
],
), patch.dict(os.environ, environment, clear=True), redirect_stderr(
io.StringIO()
), self.assertRaises(SystemExit) as raised:
runpy.run_path(str(TOOLS_DIR / "revoke-lease.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 2)
def test_gate_entrypoint_denies_when_identity_environment_is_absent(self) -> None:
# Deliberately NOT changed alongside its revoker twin above. The asymmetry is
# intentional: the gate's deny-on-absent is the authorization path and is
# load-bearing, so absent identity must fail closed here. The revoker's rc=2
# was inert in the same case (no session id means no broker call is possible),
# which is why only the revoker moved under D29. Do not "restore symmetry".
class Stdin:
buffer = io.BytesIO(b'{"tool_name":"Bash"}')
@@ -703,8 +742,11 @@ class LeaseRevocationTest(unittest.TestCase):
"MOSAIC_RUNTIME_GENERATION": "1",
}
malformed_session = {**good, "MOSAIC_LEASE_SESSION_ID": "not-a-session"}
# D29 exemption: the `({}, ...)` case was removed from this list. An empty
# environment is absence-of-lease, not an identity/reply/transport failure, and
# its correct result is no-op success (pinned in revoke_noop_unittest.py). The
# five cases below are all machinery-present failures and stay fail-closed.
cases = [
({}, lambda *_args: {"ok": True, "state": "UNVERIFIED"}),
(malformed_session, lambda *_args: {"ok": True, "state": "UNVERIFIED"}),
(good, lambda *_args: {"ok": False, "state": "UNVERIFIED"}),
(good, lambda *_args: {"ok": True, "state": "VERIFIED"}),