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:
+82
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user