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:
2026-07-07 18:07:44 -04:00
co-authored by Claude Opus 4.8
parent 54a75153a9
commit d306c0a5ec
3 changed files with 350 additions and 15 deletions
+33
View File
@@ -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