gitea_delete_branch must enforce exact gitea.branch.delete capability at tool entry #408

Closed
opened 2026-07-07 10:22:34 -05:00 by jcwalker3 · 2 comments
Owner

Evidence (PR #394 audit, 2026-07-07)

PR #394 was merged by sysadmin at 10:15:41 (merge commit fcb994437852bdb2498aeb84d111a3da79b8972d, second parent = reviewed head a164700 — merge itself safe), and the head branch feat/issue-391-review-final-report-schema was deleted from the remote afterward. Whether that deletion was capability-authorized cannot be proven, because the deployed tool does not check:

  • gitea_mcp_server.py gitea_delete_branch (≈ lines 3449–3478 on current master): body is verify_preflight_purity(remote) → resolve → auth → raw DELETE .../branches/{branch} inside an _audited("delete_branch", ...) block. No _profile_operation_gate("gitea.branch.delete") call. verify_preflight_purity only proves whoami + some capability resolve happened earlier in the session — any task's resolve satisfies it.
  • Contrast in the same file: gitea_reconcile_merged_cleanups gates the identical permission (delete_capability_allowed = not _profile_operation_gate("gitea.branch.delete"), ≈ line 3558) before deleting anything.
  • task_capability_map.py maps delete_branchgitea.branch.delete / role author, and the role classifier lists gitea.branch.delete as author-side — yet a reviewer session can invoke the raw tool successfully.

This is a wall-2 capability-parity defect (raw write tool weaker than the resolver): resolver denial does not make the underlying write tool fail closed for this operation. Every other mutating surface audited today (issue tools via ISSUE_MUTATION_TOOL_TASKS, close_pr via #216, commit_files via #262, reconcile cleanups) enforces its exact operation; gitea_delete_branch is the outlier.

Required behavior

  1. gitea_delete_branch fails closed at tool entry unless the active profile allows gitea.branch.delete (via _profile_operation_gate, same as gitea_reconcile_merged_cleanups), returning the structured #142 permission report on block with no API call made.
  2. Role routing parity: delete_branch resolver/router classification (author role) and the tool gate must agree; a reviewer-bound session calling the raw tool gets the same denial the resolver would report.
  3. Audit entry records the capability decision, not just the operation.

Acceptance criteria

  • Profile without gitea.branch.delete calling gitea_delete_branch → fail closed, no DELETE request issued, structured permission report returned.
  • Profile with the permission → deletion proceeds through existing preflight and audit unchanged.
  • Regression test proves resolver-denied delete_branch cannot execute through the raw tool (parity test alongside the existing issue-tool gate tests).
  • No profile gains gitea.branch.delete as part of this fix.

Scope relationships

  • #402 — report-layer cleanup checklist (capability proof reported); this issue is the server-side gate itself. Complementary: #402 validates the claim, this makes the unauthorized call impossible.
  • #216 (closed) — the close_pr precedent: distinct capability, gated at tool level.
  • mcp-control-plane #69/#71 wall 2 — the enforcement-parity principle this restores.

Non-goals

  • No change to merge gates (PR #394's merge path checked gitea.pr.merge correctly).
  • No broadening of any profile's operations.
## Evidence (PR #394 audit, 2026-07-07) PR #394 was merged by `sysadmin` at 10:15:41 (merge commit `fcb994437852bdb2498aeb84d111a3da79b8972d`, second parent = reviewed head `a164700` — merge itself safe), and the head branch `feat/issue-391-review-final-report-schema` was deleted from the remote afterward. Whether that deletion was capability-authorized cannot be proven, because the deployed tool does not check: * `gitea_mcp_server.py` `gitea_delete_branch` (≈ lines 3449–3478 on current master): body is `verify_preflight_purity(remote)` → resolve → auth → raw `DELETE .../branches/{branch}` inside an `_audited("delete_branch", ...)` block. **No `_profile_operation_gate("gitea.branch.delete")` call.** `verify_preflight_purity` only proves whoami + *some* capability resolve happened earlier in the session — any task's resolve satisfies it. * Contrast in the same file: `gitea_reconcile_merged_cleanups` gates the identical permission (`delete_capability_allowed = not _profile_operation_gate("gitea.branch.delete")`, ≈ line 3558) before deleting anything. * `task_capability_map.py` maps `delete_branch` → `gitea.branch.delete` / role `author`, and the role classifier lists `gitea.branch.delete` as author-side — yet a reviewer session can invoke the raw tool successfully. This is a wall-2 capability-parity defect (raw write tool weaker than the resolver): resolver denial does not make the underlying write tool fail closed for this operation. Every other mutating surface audited today (issue tools via `ISSUE_MUTATION_TOOL_TASKS`, close_pr via #216, commit_files via #262, reconcile cleanups) enforces its exact operation; `gitea_delete_branch` is the outlier. ## Required behavior 1. `gitea_delete_branch` fails closed at tool entry unless the active profile allows `gitea.branch.delete` (via `_profile_operation_gate`, same as `gitea_reconcile_merged_cleanups`), returning the structured #142 permission report on block with no API call made. 2. Role routing parity: `delete_branch` resolver/router classification (author role) and the tool gate must agree; a reviewer-bound session calling the raw tool gets the same denial the resolver would report. 3. Audit entry records the capability decision, not just the operation. ## Acceptance criteria * Profile without `gitea.branch.delete` calling `gitea_delete_branch` → fail closed, no DELETE request issued, structured permission report returned. * Profile with the permission → deletion proceeds through existing preflight and audit unchanged. * Regression test proves resolver-denied `delete_branch` cannot execute through the raw tool (parity test alongside the existing issue-tool gate tests). * No profile gains `gitea.branch.delete` as part of this fix. ## Scope relationships * **#402** — report-layer cleanup checklist (capability proof *reported*); this issue is the server-side gate itself. Complementary: #402 validates the claim, this makes the unauthorized call impossible. * **#216** (closed) — the close_pr precedent: distinct capability, gated at tool level. * **mcp-control-plane #69/#71 wall 2** — the enforcement-parity principle this restores. ## Non-goals * No change to merge gates (PR #394's merge path checked `gitea.pr.merge` correctly). * No broadening of any profile's operations.
jcwalker3 added the status:in-progress label 2026-07-07 10:28:32 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #408 - branch: feat/issue-408-delete-branch-capability-gate - phase: claimed - profile: prgs-author - pr: none - blocker: none - next_action: create worktree and begin implementation
Author
Owner

PR #388 is another example where gitea_delete_branch was used after merge without the final report clearly proving exact delete_branch / gitea.branch.delete capability. This reinforces the need for gitea_delete_branch to enforce the exact branch-delete gate at tool entry and for reports to include explicit branch-delete capability proof.

PR #388 is another example where `gitea_delete_branch` was used after merge without the final report clearly proving exact `delete_branch` / `gitea.branch.delete` capability. This reinforces the need for `gitea_delete_branch` to enforce the exact branch-delete gate at tool entry and for reports to include explicit branch-delete capability proof.
sysadmin removed the status:in-progress label 2026-07-07 10:48:53 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

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