feat: first-class control-plane lease lifecycle (Closes #601)
Make active leases queryable workflow state with canonical adopt, release, expire/reclaim, and abandon operations on the #613 control-plane DB substrate. - lease_lifecycle.py: policy, proof gates, safe_next_action vocabulary - control_plane_db: schema v3 provenance columns + list/inspect/adopt/abandon - MCP tools: gitea_*_workflow_lease(s) for list/inspect/adopt/release/expire/abandon/reclaim - issue_lock_store: sanctioned reclaim of expired author locks (dead pid / missing wt) - Tests cover lifecycle, foreign steal refusal, allocator compatibility, merger handoff
This commit is contained in:
@@ -837,6 +837,7 @@ import mcp_session_state # noqa: E402
|
||||
import stale_review_decision_lock # noqa: E402
|
||||
import allocator_service # noqa: E402
|
||||
import control_plane_db # noqa: E402
|
||||
import lease_lifecycle # noqa: E402
|
||||
import incident_bridge # noqa: E402
|
||||
import agent_temp_artifacts
|
||||
import issue_lock_worktree # noqa: E402
|
||||
@@ -11622,6 +11623,315 @@ def gitea_allocate_next_work(
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
# ── Control-plane lease lifecycle (#601) ──────────────────────────────────────
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_workflow_leases(
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
role: str | None = None,
|
||||
include_non_active: bool = False,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""List control-plane leases as first-class workflow state (#601).
|
||||
|
||||
Read-only. Returns active (default) or all leases from the #613 DB substrate.
|
||||
File locks and comment-only leases are not listed as authoritative here.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"leases": [],
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
try:
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
except ValueError as exc:
|
||||
return {"success": False, "reasons": [str(exc)], "leases": []}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs, "leases": []}
|
||||
result = lease_lifecycle.list_active_leases(
|
||||
db,
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
role=role,
|
||||
include_non_active=bool(include_non_active),
|
||||
limit=int(limit),
|
||||
)
|
||||
result["remote"] = remote
|
||||
result["org"] = o
|
||||
result["repo"] = r
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_inspect_workflow_lease(
|
||||
lease_id: str,
|
||||
session_id: str | None = None,
|
||||
worktree_path: str | None = None,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Inspect one control-plane lease with safe_next_action (#601).
|
||||
|
||||
Distinguishes owner-resume, wait-foreign, reclaim-expired, abandon-allowed,
|
||||
and stale prompt ids. Control-plane DB is authoritative.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
profile = get_profile()
|
||||
profile_name = (profile.get("profile_name") or "").strip() or "session"
|
||||
sid = (session_id or "").strip() or f"{profile_name}-{os.getpid()}"
|
||||
return lease_lifecycle.inspect_lease(
|
||||
db,
|
||||
lease_id,
|
||||
caller_session_id=sid,
|
||||
caller_worktree=worktree_path,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_adopt_workflow_lease(
|
||||
lease_id: str,
|
||||
session_id: str | None = None,
|
||||
role: str | None = None,
|
||||
worktree_path: str | None = None,
|
||||
expected_head_sha: str | None = None,
|
||||
operator_authorized: bool = False,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Adopt a control-plane lease through the sanctioned path (#601).
|
||||
|
||||
Same-owner resume refreshes provenance. Foreign active leases are refused.
|
||||
Expired leases may be reclaimed; provenance records adopted_from/by.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
profile = get_profile()
|
||||
profile_name = (profile.get("profile_name") or "").strip() or "session"
|
||||
active_role = _profile_role_kind(profile) or "author"
|
||||
sid = (session_id or "").strip() or (
|
||||
f"{profile_name}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
||||
)
|
||||
try:
|
||||
return lease_lifecycle.adopt_lease(
|
||||
db,
|
||||
lease_id=lease_id,
|
||||
adopter_session_id=sid,
|
||||
role=(role or active_role).strip() or "author",
|
||||
worktree_path=worktree_path,
|
||||
expected_head_sha=expected_head_sha,
|
||||
owner_pid=os.getpid(),
|
||||
operator_authorized=bool(operator_authorized),
|
||||
)
|
||||
except (lease_lifecycle.LeaseLifecycleError, control_plane_db.ControlPlaneError) as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"outcome": "blocked",
|
||||
"reasons": [_redact(str(exc))],
|
||||
"lease_id": lease_id,
|
||||
"authoritative_source": "control_plane_db",
|
||||
"file_lock_only": False,
|
||||
"comment_lease_only": False,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_release_workflow_lease(
|
||||
lease_id: str,
|
||||
session_id: str,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Explicitly release a control-plane lease owned by *session_id* (#601).
|
||||
|
||||
Recorded in the DB event log with release provenance. Foreign release fails closed.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
try:
|
||||
return lease_lifecycle.release_lease(
|
||||
db, lease_id=lease_id, session_id=session_id
|
||||
)
|
||||
except (lease_lifecycle.LeaseLifecycleError, control_plane_db.ControlPlaneError) as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"outcome": "blocked",
|
||||
"reasons": [_redact(str(exc))],
|
||||
"lease_id": lease_id,
|
||||
"authoritative_source": "control_plane_db",
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_expire_workflow_leases(
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Expire stale control-plane leases whose expires_at has passed (#601).
|
||||
|
||||
Deterministic reclaim prerequisite. Does not steal active non-expired leases.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
return lease_lifecycle.expire_leases(db)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_abandon_workflow_lease(
|
||||
lease_id: str,
|
||||
session_id: str | None = None,
|
||||
dead_process: bool = False,
|
||||
missing_worktree: bool = False,
|
||||
no_open_pr: bool = False,
|
||||
no_live_mutation_risk: bool = False,
|
||||
operator_authorized: bool = False,
|
||||
worktree_path: str | None = None,
|
||||
owner_pid: int | None = None,
|
||||
notes: str = "",
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Abandon a control-plane lease with required proof (#601).
|
||||
|
||||
Requires (dead_process or missing_worktree) and no_live_mutation_risk.
|
||||
Foreign abandon also needs operator_authorized or
|
||||
(dead_process and missing_worktree and no_open_pr).
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
profile = get_profile()
|
||||
profile_name = (profile.get("profile_name") or "").strip() or "session"
|
||||
sid = (session_id or "").strip() or f"{profile_name}-{os.getpid()}"
|
||||
proof = lease_lifecycle.AbandonProof(
|
||||
dead_process=bool(dead_process),
|
||||
missing_worktree=bool(missing_worktree),
|
||||
no_open_pr=bool(no_open_pr),
|
||||
no_live_mutation_risk=bool(no_live_mutation_risk),
|
||||
operator_authorized=bool(operator_authorized),
|
||||
worktree_path=worktree_path,
|
||||
owner_pid=owner_pid,
|
||||
notes=notes or "",
|
||||
)
|
||||
try:
|
||||
return lease_lifecycle.abandon_lease(
|
||||
db,
|
||||
lease_id=lease_id,
|
||||
requester_session_id=sid,
|
||||
proof=proof,
|
||||
)
|
||||
except (lease_lifecycle.LeaseLifecycleError, control_plane_db.ControlPlaneError) as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"outcome": "blocked",
|
||||
"reasons": [_redact(str(exc))],
|
||||
"lease_id": lease_id,
|
||||
"authoritative_source": "control_plane_db",
|
||||
"file_lock_only": False,
|
||||
"comment_lease_only": False,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_reclaim_expired_workflow_lease(
|
||||
lease_id: str,
|
||||
session_id: str | None = None,
|
||||
role: str | None = None,
|
||||
worktree_path: str | None = None,
|
||||
expected_head_sha: str | None = None,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
) -> dict:
|
||||
"""Reclaim an expired control-plane lease for a new/owner session (#601).
|
||||
|
||||
Deterministic: expire markers applied, then atomic assign+lease with provenance.
|
||||
Active foreign leases are refused.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
db, errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
return {"success": False, "reasons": errs}
|
||||
profile = get_profile()
|
||||
profile_name = (profile.get("profile_name") or "").strip() or "session"
|
||||
active_role = _profile_role_kind(profile) or "author"
|
||||
sid = (session_id or "").strip() or (
|
||||
f"{profile_name}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
||||
)
|
||||
try:
|
||||
return lease_lifecycle.reclaim_expired_lease(
|
||||
db,
|
||||
lease_id=lease_id,
|
||||
session_id=sid,
|
||||
role=(role or active_role).strip() or "author",
|
||||
worktree_path=worktree_path,
|
||||
expected_head_sha=expected_head_sha,
|
||||
)
|
||||
except (lease_lifecycle.LeaseLifecycleError, control_plane_db.ControlPlaneError) as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"outcome": "blocked",
|
||||
"reasons": [_redact(str(exc))],
|
||||
"lease_id": lease_id,
|
||||
"authoritative_source": "control_plane_db",
|
||||
}
|
||||
|
||||
|
||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user