feat: harden keyed issue lock store with flock serialization (#438)
Rebase onto master (#443 keyed store) and port #438 hardening: per-issue fcntl.flock around bind_session_lock, assess_lock_freshness with dead-PID detection, verify_lock_for_mutation before create_pr, and live lock inventory for claim visibility. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -1382,6 +1382,19 @@ def gitea_lock_issue(
|
||||
}
|
||||
|
||||
lock_file_path = _save_issue_lock(data)
|
||||
lock_record = issue_lock_store.read_lock_file(lock_file_path) or data
|
||||
freshness = issue_lock_store.assess_lock_freshness(lock_record)
|
||||
competing = [
|
||||
entry
|
||||
for entry in issue_lock_store.list_live_locks()
|
||||
if entry.get("issue_number") != issue_number
|
||||
]
|
||||
lock_proof = issue_lock_store.format_lock_proof(
|
||||
lock_record,
|
||||
freshness=freshness,
|
||||
competing_live_locks=competing,
|
||||
released=False,
|
||||
)
|
||||
|
||||
agent_artifacts = agent_temp_artifacts.find_agent_temp_artifacts_from_porcelain(
|
||||
git_state.get("porcelain_status") or ""
|
||||
@@ -1397,6 +1410,8 @@ def gitea_lock_issue(
|
||||
"worktree_path": resolved_worktree,
|
||||
"work_lease": work_lease,
|
||||
"lock_file_path": lock_file_path,
|
||||
"lock_freshness": freshness,
|
||||
"lock_proof": lock_proof,
|
||||
}
|
||||
if adoption["adopt"]:
|
||||
result["adoption"] = issue_lock_adoption.build_adoption_proof(
|
||||
@@ -1529,6 +1544,15 @@ def gitea_create_pr(
|
||||
f"PR head branch '{head}' does not match locked branch '{locked_branch}' (fail closed)"
|
||||
)
|
||||
|
||||
ownership = issue_lock_store.verify_lock_for_mutation(
|
||||
lock_data,
|
||||
issue_number=locked_issue,
|
||||
branch_name=head,
|
||||
worktree_path=worktree_path,
|
||||
)
|
||||
if ownership["block"]:
|
||||
raise ValueError(ownership["reasons"][0])
|
||||
|
||||
# Check for forbidden terms anywhere in title/body
|
||||
forbidden_terms = ["equivalent", "related", "same as"]
|
||||
text_to_check = f"{title} {body}".lower()
|
||||
@@ -6691,6 +6715,15 @@ def gitea_reconcile_issue_claims(
|
||||
heartbeat_lease_minutes=heartbeat_lease_minutes,
|
||||
reclaim_after_minutes=reclaim_after_minutes,
|
||||
)
|
||||
live_locks = issue_lock_store.list_live_locks()
|
||||
inventory["live_issue_locks"] = live_locks
|
||||
inventory["live_issue_lock_numbers"] = sorted(
|
||||
{
|
||||
int(entry["issue_number"])
|
||||
for entry in live_locks
|
||||
if entry.get("issue_number") is not None
|
||||
}
|
||||
)
|
||||
inventory["cleanup_plan"] = issue_claim_heartbeat.build_cleanup_plan(inventory)
|
||||
inventory["success"] = True
|
||||
inventory["performed"] = False
|
||||
|
||||
Reference in New Issue
Block a user