fix(reconciler): forward explicit repository through delete_branch anti-stomp preflight (Closes #733)

gitea_delete_branch accepted explicit org/repo but did not propagate them
through the anti-stomp preflight. During a deletion targeting
Scaled-Tech-Consulting/Gitea-Tools, preflight resolved the remote-wide prgs
default (Scaled-Tech-Consulting/Timesheet) and failed closed with wrong_repo
before any deletion — because verify_preflight_purity was called without
org/repo and the shared #604 anti-stomp resolution fell back to the REMOTES
default.

Fix (delete_branch only; REMOTES untouched):

1. Forward the explicit org/repo into verify_preflight_purity so the shared
   #604 anti-stomp resolution validates the *targeted* repository instead of
   the remote-wide default. Explicit Scaled-Tech-Consulting/Gitea-Tools now
   propagates through role/workspace verification, verify_preflight_purity,
   anti-stomp repository resolution, and the final native deletion request.

2. Add a workspace-derived repository-binding gate
   (_delete_branch_repository_binding_block) that validates explicit
   coordinates against the immutable, workspace-aligned repository identity.
   Wrong, substituted, or unverified coordinates fail closed — independent of
   the anti-stomp remote/repo guard, which by the #530 contract trusts explicit
   caller intent. REMOTES defaults are never consulted as an authorization
   scope.

Preserved: reconciler-only ownership of gitea.branch.delete; author/reviewer/
merger denial; protected and preservation/evidence gates; missing coordinates
still fail closed via the anti-stomp default resolution.

Regression matrix (tests/test_issue_733_delete_branch_repo_forwarding.py):
Timesheet-default + explicit Gitea-Tools permits an eligible deletion and
forwards the coordinates; wrong/substituted/unverified coordinates fail closed;
author/reviewer/merger remain denied; protected and preservation branches
remain blocked; anti-stomp resolution marks explicit coordinates authoritative
and fails closed on the remote-wide default for omitted coordinates; the
task-capability and role-routing maps stay consistent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UVDxVKANhuGYzZgayDU3QS
This commit is contained in:
2026-07-17 22:45:18 -04:00
co-authored by Claude Opus 4.8
parent daf60266d0
commit 3990fc684f
2 changed files with 467 additions and 1 deletions
+82 -1
View File
@@ -8562,6 +8562,67 @@ def gitea_review_pr(
return out
def _delete_branch_repository_binding_block(
remote: str | None,
*,
org: str | None,
repo: str | None,
) -> dict | None:
"""#733: validate an explicit delete target against the workspace binding.
The trusted repository identity comes only from the verified,
workspace-aligned git remote (never ``REMOTES`` defaults, never
unvalidated caller input). Explicit ``org``/``repo`` are a *request target*
checked against that binding:
* matching or omitted coordinates pass omitted targets are still resolved
and re-validated by the shared #604 anti-stomp preflight, which fails
closed on a remote-wide default mismatch (bare prgs Timesheet);
* coordinates that disagree with the binding are rejected (wrong or
substituted repository);
* explicit coordinates with no establishable workspace identity fail closed
(unproven target).
Returns a structured block dict, or ``None`` when the target is authorized.
"""
workspace_slug = _workspace_repository_slug(remote)
workspace_parts = (
session_ctx.parse_repository_slug(workspace_slug) if workspace_slug else None
)
if workspace_parts:
override = session_ctx.assess_repository_override(
requested_org=org,
requested_repo=repo,
bound_org=workspace_parts[0],
bound_repo=workspace_parts[1],
)
if override.get("block"):
return {
"success": False,
"performed": False,
"required_permission": "gitea.branch.delete",
"reasons": list(override.get("reasons") or [
"delete target repository does not match the workspace "
"binding (fail closed)"
]),
"blocker_kind": "repository_binding",
}
return None
if org is not None or repo is not None:
return {
"success": False,
"performed": False,
"required_permission": "gitea.branch.delete",
"reasons": [
"repository binding unverified: no workspace repository "
"identity could be established to corroborate the explicit "
"delete target (fail closed)"
],
"blocker_kind": "repository_binding",
}
return None
@mcp.tool()
def gitea_delete_branch(
branch: str,
@@ -8652,8 +8713,28 @@ def gitea_delete_branch(
"audit_phase": audit_reconciliation_mode.current_phase(),
}
# #733: the explicitly targeted repository must agree with the immutable,
# workspace-derived repository identity before any preflight or deletion.
# This rejects wrong or substituted coordinates (e.g. an explicit Timesheet
# target while the workspace is bound to Gitea-Tools) and fails closed when
# explicit coordinates cannot be corroborated against a trusted workspace
# identity. It is independent of the anti-stomp remote/repo guard, which by
# the #530 contract trusts explicit caller intent; here explicit intent is
# validated, not trusted. REMOTES defaults are never consulted as an
# authorization scope (#530/#714).
repo_binding_block = _delete_branch_repository_binding_block(
remote, org=org, repo=repo,
)
if repo_binding_block:
return repo_binding_block
with _mutation_stage("preflight_purity"):
verify_preflight_purity(remote, task="delete_branch")
# #733: forward the explicit org/repo through verify_preflight_purity so
# the shared #604 anti-stomp resolution validates the *targeted*
# repository instead of falling back to the remote-wide REMOTES default
# (bare prgs → Timesheet), which false-positives wrong_repo and blocked
# every explicit Scaled-Tech-Consulting/Gitea-Tools deletion.
verify_preflight_purity(remote, task="delete_branch", org=org, repo=repo)
with _mutation_stage("resolve_auth"):
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)