Bind post-merge moot-lease cleanup to the reconciler capability #745

Open
opened 2026-07-18 11:42:43 -05:00 by jcwalker3 · 1 comment
Owner

Problem

gitea_cleanup_post_merge_moot_lease (#515) can perform a real PR-comment mutation — it posts a terminal phase: released lease marker — but it has no entry in the canonical task-capability map and no role binding. It is the only comment-mutating lease tool without one.

Every sibling lease/cleanup mutation is mapped and role-bound in task_capability_map.TASK_CAPABILITY_MAP:

  • cleanup_obsolete_reviewer_comment_lease / gitea_... -> gitea.pr.comment + reviewer (#691)
  • release_merger_pr_lease / gitea_... -> gitea.pr.comment + merger (#742)
  • quarantine_contaminated_review / gitea_... -> gitea.pr.comment + reconciler (#695 AC8)
  • delete_branch, cleanup_merged_pr_branch -> gitea.branch.delete + reconciler (#729)

cleanup_post_merge_moot_lease is absent from that map entirely.

Current behavior

In gitea_mcp_server.gitea_cleanup_post_merge_moot_lease:

  • entry gate is _profile_operation_gate("gitea.read");
  • the apply path gates only on _profile_operation_gate("gitea.pr.comment");
  • there is no resolvable cleanup task, so gitea_resolve_task_capability("cleanup_post_merge_moot_lease") fails closed as an unknown task and the operator-required resolve-exact-task -> mutation sequence cannot be satisfied by any caller;
  • there is no required-role gate, so any profile holding gitea.pr.comment (author, reviewer, merger) reaches the mutation path;
  • verify_preflight_purity(remote) is called with no task=, no org=, no repo=, so the resolved-task, canonical-root and explicit-target checks that gitea_delete_branch and gitea_cleanup_merged_pr_branch perform (#733/#739) are skipped here;
  • request org/repo parameters flow straight into _resolve(...), so a caller can retarget the mutation at a foreign repository.

Net effect: a permission-only mutation surface over durable lease audit state, unreachable by the role that should own it and reachable by three roles that should not.

Expected behavior

  • A canonical task cleanup_post_merge_moot_lease resolves to permission=gitea.pr.comment, role=reconciler.
  • The tool-name alias gitea_cleanup_post_merge_moot_lease resolves to the same contract (project convention for every aliased task), never a diverging one.
  • Author, reviewer and merger fail closed on the apply path even though their profiles may hold gitea.pr.comment.
  • Unknown or misspelled task names remain rejected by the map's fail-closed KeyError.
  • The apply path requires, in order: exact resolved cleanup task -> reconciler role -> gitea.pr.comment -> repository/canonical-root/workspace binding -> matching dry-run evidence.
  • Dry-run evidence must show lease_moot=true, cleanup_allowed=true and an exact PR / session / candidate-head / lease-marker match before a terminal marker may be posted.
  • Live, non-moot, superseded, mismatched, malformed, foreign-repository and ambiguous leases stay fail-closed.
  • The lease ledger stays append-only; no comment is ever edited or deleted.
  • Repeat cleanup on an already-terminal lease stays idempotent (no second marker).
  • Request parameters must not override the canonical repository identity.

Read-only assessment (apply=false)

Sibling convention is explicit and consistent. gitea_cleanup_stale_review_decision_lock and gitea_cleanup_obsolete_reviewer_comment_lease both gate entry on gitea.read alone with no role check, and enforce the dedicated permission only on the apply branch; the map comment for cleanup_stale_review_decision_lock states "Apply path requires reviewer review permission; assessment itself uses gitea.read inside the tool."

The apply=false assessment path therefore stays reachable under gitea.read for any role, so an operator can diagnose a stuck lease from any namespace. This choice is documented in the map and in the tool docstring, and is directly tested. Only apply=true requires the reconciler role and the exact cleanup capability.

Acceptance criteria

  • AC1: cleanup_post_merge_moot_lease maps to gitea.pr.comment + reconciler.
  • AC2: gitea_cleanup_post_merge_moot_lease maps to an identical contract.
  • AC3: Reconciler resolves the canonical cleanup task; author, reviewer and merger do not.
  • AC4: Apply requires an exact resolved cleanup task; resolving a different task does not authorize cleanup.
  • AC5: Author, reviewer and merger cannot apply despite holding gitea.pr.comment.
  • AC6: Apply requires a matching allowed dry run (lease_moot, cleanup_allowed, PR/session/head/marker match).
  • AC7: Dry run performs no mutation and stays available under gitea.read.
  • AC8: Wrong repository, canonical root, worktree, PR, head, session or marker fails closed.
  • AC9: Non-moot and superseded leases fail closed; already-terminal cleanup is idempotent.
  • AC10: The ledger remains append-only and existing sanctioned reconciler cleanup behavior does not regress.

Safety

Do not grant reconciler-only cleanup to prgs-author. Do not clean the live PR #744 lease as a test; it must expire naturally. No direct API, curl, CLI, database or Web UI fallback.

References

## Problem `gitea_cleanup_post_merge_moot_lease` (#515) can perform a real PR-comment mutation — it posts a terminal `phase: released` lease marker — but it has **no entry in the canonical task-capability map** and **no role binding**. It is the only comment-mutating lease tool without one. Every sibling lease/cleanup mutation is mapped and role-bound in `task_capability_map.TASK_CAPABILITY_MAP`: - `cleanup_obsolete_reviewer_comment_lease` / `gitea_...` -> `gitea.pr.comment` + `reviewer` (#691) - `release_merger_pr_lease` / `gitea_...` -> `gitea.pr.comment` + `merger` (#742) - `quarantine_contaminated_review` / `gitea_...` -> `gitea.pr.comment` + `reconciler` (#695 AC8) - `delete_branch`, `cleanup_merged_pr_branch` -> `gitea.branch.delete` + `reconciler` (#729) `cleanup_post_merge_moot_lease` is absent from that map entirely. ## Current behavior In `gitea_mcp_server.gitea_cleanup_post_merge_moot_lease`: - entry gate is `_profile_operation_gate("gitea.read")`; - the apply path gates only on `_profile_operation_gate("gitea.pr.comment")`; - there is **no** resolvable cleanup task, so `gitea_resolve_task_capability("cleanup_post_merge_moot_lease")` fails closed as an unknown task and the operator-required `resolve-exact-task -> mutation` sequence cannot be satisfied by any caller; - there is **no** required-role gate, so **any** profile holding `gitea.pr.comment` (author, reviewer, merger) reaches the mutation path; - `verify_preflight_purity(remote)` is called with no `task=`, no `org=`, no `repo=`, so the resolved-task, canonical-root and explicit-target checks that `gitea_delete_branch` and `gitea_cleanup_merged_pr_branch` perform (#733/#739) are skipped here; - request `org`/`repo` parameters flow straight into `_resolve(...)`, so a caller can retarget the mutation at a foreign repository. Net effect: a permission-only mutation surface over durable lease audit state, unreachable by the role that should own it and reachable by three roles that should not. ## Expected behavior - A canonical task `cleanup_post_merge_moot_lease` resolves to `permission=gitea.pr.comment`, `role=reconciler`. - The tool-name alias `gitea_cleanup_post_merge_moot_lease` resolves to the **same** contract (project convention for every aliased task), never a diverging one. - Author, reviewer and merger fail closed on the apply path even though their profiles may hold `gitea.pr.comment`. - Unknown or misspelled task names remain rejected by the map's fail-closed `KeyError`. - The apply path requires, in order: exact resolved cleanup task -> reconciler role -> `gitea.pr.comment` -> repository/canonical-root/workspace binding -> matching dry-run evidence. - Dry-run evidence must show `lease_moot=true`, `cleanup_allowed=true` and an exact PR / session / candidate-head / lease-marker match before a terminal marker may be posted. - Live, non-moot, superseded, mismatched, malformed, foreign-repository and ambiguous leases stay fail-closed. - The lease ledger stays append-only; no comment is ever edited or deleted. - Repeat cleanup on an already-terminal lease stays idempotent (no second marker). - Request parameters must not override the canonical repository identity. ## Read-only assessment (`apply=false`) Sibling convention is explicit and consistent. `gitea_cleanup_stale_review_decision_lock` and `gitea_cleanup_obsolete_reviewer_comment_lease` both gate entry on `gitea.read` alone with no role check, and enforce the dedicated permission only on the apply branch; the map comment for `cleanup_stale_review_decision_lock` states "Apply path requires reviewer review permission; assessment itself uses gitea.read inside the tool." The `apply=false` assessment path therefore **stays reachable under `gitea.read` for any role**, so an operator can diagnose a stuck lease from any namespace. This choice is documented in the map and in the tool docstring, and is directly tested. Only `apply=true` requires the reconciler role and the exact cleanup capability. ## Acceptance criteria - AC1: `cleanup_post_merge_moot_lease` maps to `gitea.pr.comment` + `reconciler`. - AC2: `gitea_cleanup_post_merge_moot_lease` maps to an identical contract. - AC3: Reconciler resolves the canonical cleanup task; author, reviewer and merger do not. - AC4: Apply requires an exact resolved cleanup task; resolving a different task does not authorize cleanup. - AC5: Author, reviewer and merger cannot apply despite holding `gitea.pr.comment`. - AC6: Apply requires a matching allowed dry run (`lease_moot`, `cleanup_allowed`, PR/session/head/marker match). - AC7: Dry run performs no mutation and stays available under `gitea.read`. - AC8: Wrong repository, canonical root, worktree, PR, head, session or marker fails closed. - AC9: Non-moot and superseded leases fail closed; already-terminal cleanup is idempotent. - AC10: The ledger remains append-only and existing sanctioned reconciler cleanup behavior does not regress. ## Safety Do not grant reconciler-only cleanup to `prgs-author`. Do not clean the live PR #744 lease as a test; it must expire naturally. No direct API, curl, CLI, database or Web UI fallback. ## References - Tool origin: #515 - Sibling role-bound cleanups: #691, #742, #695, #729 - Capability-map / router invariant defects: #723 (adjacent; does not add this mapping) - Canonical-root and explicit-target binding precedent: #733, #739
jcwalker3 added the workflow-hardeninganti-stompsafetytype:bugstatus:in-progress labels 2026-07-18 11:43:04 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #745 - branch: fix/issue-745-reconciler-moot-lease-gate - phase: claimed - profile: prgs-author - pr: none - blocker: none - next_action: create worktree and begin implementation
jcwalker3 added status:pr-open and removed status:in-progress labels 2026-07-18 12:01:00 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#745