Merge branch 'master' into feat/issue-607-sentry-incident-bridge
This commit is contained in:
+122
-63
@@ -701,6 +701,19 @@ def _clear_preflight_capability_state() -> None:
|
||||
_preflight_reviewer_violation_files = []
|
||||
|
||||
|
||||
def _clear_resolved_capability_stamp() -> None:
|
||||
"""Clear the task/role stamp without manufacturing capability proof.
|
||||
|
||||
A fresh resolver attempt invalidates the prior stamp immediately. The
|
||||
new role/task is recorded only after the current attempt is permitted, so
|
||||
denial or an unexpected exception cannot leave stale authority behind.
|
||||
"""
|
||||
global _preflight_resolved_role, _preflight_resolved_task
|
||||
|
||||
_preflight_resolved_role = None
|
||||
_preflight_resolved_task = None
|
||||
|
||||
|
||||
def _invalidate_preflight_identity_state() -> None:
|
||||
"""Fail closed when whoami cannot prove the configured identity."""
|
||||
global _preflight_whoami_called, _preflight_whoami_violation
|
||||
@@ -5240,13 +5253,6 @@ def _evaluate_pr_review_submission(
|
||||
worktree_path: str | None = None,
|
||||
) -> dict:
|
||||
"""Shared gate chain for live submit and dry-run review tools."""
|
||||
_verify_role_mutation_workspace(
|
||||
remote,
|
||||
worktree_path=worktree_path,
|
||||
task="review_pr",
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
action = (action or "").strip().lower()
|
||||
workflow_blockers = _review_workflow_load_gate_reasons() if live else []
|
||||
result = {
|
||||
@@ -5264,6 +5270,21 @@ def _evaluate_pr_review_submission(
|
||||
"reasons": [],
|
||||
}
|
||||
reasons = result["reasons"]
|
||||
try:
|
||||
_verify_role_mutation_workspace(
|
||||
remote,
|
||||
worktree_path=worktree_path,
|
||||
task="review_pr",
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
result["blocker_kind"] = "workspace_role_binding"
|
||||
reasons.append(
|
||||
"workspace/role binding failed (fail closed, #723): "
|
||||
f"{_redact(str(exc))}"
|
||||
)
|
||||
return result
|
||||
if workflow_blockers:
|
||||
reasons.extend(workflow_blockers)
|
||||
reasons.extend(review_workflow_load.recovery_handoff_without_replay())
|
||||
@@ -12085,14 +12106,31 @@ def gitea_adopt_merger_pr_lease(
|
||||
"permission_report": _permission_block_report("gitea.pr.merge"),
|
||||
}
|
||||
|
||||
# task=adopt_merger_pr_lease so verify_preflight_purity runs shared #604 anti-stomp.
|
||||
_verify_role_mutation_workspace(
|
||||
remote,
|
||||
worktree=worktree,
|
||||
task="adopt_merger_pr_lease",
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
# task=adopt_merger_pr_lease so verify_preflight_purity runs shared #604
|
||||
# anti-stomp. A role/workspace rejection is an expected guarded outcome,
|
||||
# not an opaque MCP internal_error (#723).
|
||||
try:
|
||||
_verify_role_mutation_workspace(
|
||||
remote,
|
||||
worktree=worktree,
|
||||
task="adopt_merger_pr_lease",
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"adopted": False,
|
||||
"pr_number": pr_number,
|
||||
"blocker_kind": "workspace_role_binding",
|
||||
"reasons": [
|
||||
"workspace/role binding failed (fail closed, #723): "
|
||||
f"{_redact(str(exc))}"
|
||||
],
|
||||
"active_lease": None,
|
||||
"expected_head_sha": expected_head_sha,
|
||||
"live_head_sha": None,
|
||||
}
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
profile = get_profile()
|
||||
@@ -14307,11 +14345,15 @@ def _matching_configured_profiles(
|
||||
config: dict | None,
|
||||
required_permission: str,
|
||||
remote: str | None = None,
|
||||
required_role_kind: str | None = None,
|
||||
) -> list[str]:
|
||||
"""Profile names that allow *required_permission* (redacted metadata only).
|
||||
|
||||
#714: when *remote* is provided, only profiles bound to that remote's host
|
||||
are returned — a dadeschools request never lists prgs profiles.
|
||||
are returned — a dadeschools request never lists prgs profiles. For a
|
||||
role-exclusive task, a profile that declares a role must also declare the
|
||||
required role (#723); undeclared legacy profiles retain permission-only
|
||||
compatibility.
|
||||
"""
|
||||
if not config or "profiles" not in config:
|
||||
return []
|
||||
@@ -14349,7 +14391,15 @@ def _matching_configured_profiles(
|
||||
ok, _ = gitea_config.check_operation(
|
||||
required_permission, p_allowed_n, p_forbidden_n
|
||||
)
|
||||
if ok:
|
||||
declared_role = (
|
||||
p_data.get("role") or p_data.get("role_kind") or ""
|
||||
).strip()
|
||||
role_allowed = (
|
||||
required_role_kind is None
|
||||
or not declared_role
|
||||
or declared_role == required_role_kind
|
||||
)
|
||||
if ok and role_allowed:
|
||||
matches.append(p_name)
|
||||
return sorted(matches)
|
||||
|
||||
@@ -14359,8 +14409,15 @@ def _build_runtime_task_capabilities(
|
||||
forbidden: list[str],
|
||||
config: dict | None,
|
||||
remote: str | None = None,
|
||||
active_role_kind: str | None = None,
|
||||
) -> dict:
|
||||
"""Per-task capability summary for role-aware runtime context (#139, #714)."""
|
||||
"""Per-task capability summary for role-aware runtime context.
|
||||
|
||||
A declared active role produces a ``role_filtered`` view consistent with
|
||||
the resolver. Callers that deliberately omit the role receive the legacy
|
||||
``permission_only`` view, labeled as such rather than implying stronger
|
||||
authority (#723).
|
||||
"""
|
||||
task_entries = []
|
||||
flags: dict[str, bool] = {}
|
||||
flag_keys = {
|
||||
@@ -14373,18 +14430,36 @@ def _build_runtime_task_capabilities(
|
||||
"close_issue": "can_close_issues",
|
||||
"reconcile_already_landed_pr": "can_reconcile_already_landed_prs",
|
||||
}
|
||||
role_filter_applied = active_role_kind is not None
|
||||
for task in _RUNTIME_CAPABILITY_TASKS:
|
||||
permission = task_capability_map.required_permission(task)
|
||||
allowed_here, _ = gitea_config.check_operation(
|
||||
required_role_kind = task_capability_map.required_role(task)
|
||||
role_exclusive = task in task_capability_map.ROLE_EXCLUSIVE_TASKS
|
||||
permission_allowed, _ = gitea_config.check_operation(
|
||||
permission, allowed, forbidden
|
||||
)
|
||||
role_allowed = (
|
||||
not role_exclusive
|
||||
or not role_filter_applied
|
||||
or active_role_kind == required_role_kind
|
||||
)
|
||||
allowed_here = permission_allowed and role_allowed
|
||||
entry = {
|
||||
"task": task,
|
||||
"required_permission": permission,
|
||||
"required_role_kind": task_capability_map.required_role(task),
|
||||
"required_role_kind": required_role_kind,
|
||||
"role_exclusive": role_exclusive,
|
||||
"capability_view": (
|
||||
"role_filtered" if role_filter_applied else "permission_only"
|
||||
),
|
||||
"allowed_in_current_session": allowed_here,
|
||||
"matching_configured_profiles": _matching_configured_profiles(
|
||||
config, permission, remote=remote
|
||||
config,
|
||||
permission,
|
||||
remote=remote,
|
||||
required_role_kind=(
|
||||
required_role_kind if role_exclusive else None
|
||||
),
|
||||
),
|
||||
}
|
||||
task_entries.append(entry)
|
||||
@@ -14848,7 +14923,11 @@ def gitea_get_runtime_context(
|
||||
|
||||
# #714: filter matching profiles by requested remote when building capability summary
|
||||
session_capabilities = _build_runtime_task_capabilities(
|
||||
allowed, forbidden, config, remote=remote if remote in REMOTES else None
|
||||
allowed,
|
||||
forbidden,
|
||||
config,
|
||||
remote=remote if remote in REMOTES else None,
|
||||
active_role_kind=_profile_role_kind(profile),
|
||||
)
|
||||
|
||||
preflight = assess_preflight_status(worktree_path)
|
||||
@@ -17308,6 +17387,10 @@ def gitea_resolve_task_capability(
|
||||
host: Optional override for the Gitea host.
|
||||
"""
|
||||
TASK_MAP = task_capability_map.TASK_CAPABILITY_MAP
|
||||
# Every fresh attempt invalidates the previous task/role stamp before any
|
||||
# fallible resolver work. Unknown/malformed tasks and unexpected failures
|
||||
# therefore remain fail-closed instead of preserving stale authority.
|
||||
_clear_resolved_capability_stamp()
|
||||
|
||||
if task not in TASK_MAP:
|
||||
# #723: structured fail-closed unknown_task (never raise into internal_error).
|
||||
@@ -17357,36 +17440,7 @@ def gitea_resolve_task_capability(
|
||||
|
||||
required_permission = task_capability_map.required_permission(task)
|
||||
required_role = task_capability_map.required_role(task)
|
||||
role_exclusive_tasks = {
|
||||
"acquire_reviewer_pr_lease",
|
||||
"gitea_acquire_reviewer_pr_lease",
|
||||
"review_pr",
|
||||
"approve_pr",
|
||||
"request_changes_pr",
|
||||
"blind_pr_queue_review",
|
||||
"pr_queue_cleanup",
|
||||
"pr-queue-cleanup",
|
||||
"merge_pr",
|
||||
"acquire_merger_pr_lease",
|
||||
"gitea_acquire_merger_pr_lease",
|
||||
"adopt_merger_pr_lease",
|
||||
"gitea_adopt_merger_pr_lease",
|
||||
"release_merger_pr_lease",
|
||||
"gitea_release_merger_pr_lease",
|
||||
"create_branch",
|
||||
"push_branch",
|
||||
"create_pr",
|
||||
"commit_files",
|
||||
"gitea_commit_files",
|
||||
"address_pr_change_requests",
|
||||
"update_pr_branch_by_merge",
|
||||
"gitea_update_pr_branch_by_merge",
|
||||
"delete_branch",
|
||||
"cleanup_merged_pr_branch",
|
||||
"reconciliation_cleanup",
|
||||
"work_issue",
|
||||
"work-issue",
|
||||
}
|
||||
role_exclusive_tasks = task_capability_map.ROLE_EXCLUSIVE_TASKS
|
||||
|
||||
infra_assessment = role_session_router.assess_infra_stop(PROJECT_ROOT)
|
||||
if required_role == "reviewer":
|
||||
@@ -17484,7 +17538,9 @@ def gitea_resolve_task_capability(
|
||||
blocked_result["stale_binding_recovery"] = stale_binding
|
||||
return blocked_result
|
||||
|
||||
record_preflight_check("capability", required_role, resolved_task=task)
|
||||
# Record the purity baseline now, but do not record a role/task authority
|
||||
# stamp until the final allow decision is known (#723).
|
||||
record_preflight_check("capability")
|
||||
|
||||
# #714: never auto-switch profiles during capability resolution.
|
||||
profile = get_profile()
|
||||
@@ -17557,17 +17613,13 @@ def gitea_resolve_task_capability(
|
||||
|
||||
# Matching profiles: same remote/host only (#714) — advisory, never activated.
|
||||
matching_profiles = _matching_configured_profiles(
|
||||
config, required_permission, remote=remote
|
||||
config,
|
||||
required_permission,
|
||||
remote=remote,
|
||||
required_role_kind=(
|
||||
required_role if task in role_exclusive_tasks else None
|
||||
),
|
||||
)
|
||||
# Role filter for exclusive tasks
|
||||
if config and "profiles" in config and task in role_exclusive_tasks:
|
||||
filtered = []
|
||||
for p_name in matching_profiles:
|
||||
p_data = (config.get("profiles") or {}).get(p_name) or {}
|
||||
p_role = (p_data.get("role") or "").strip()
|
||||
if not p_role or p_role == required_role:
|
||||
filtered.append(p_name)
|
||||
matching_profiles = filtered
|
||||
|
||||
configured = len(matching_profiles) > 0
|
||||
available_in_session = allowed_in_current_session
|
||||
@@ -17782,6 +17834,13 @@ def gitea_resolve_task_capability(
|
||||
# A denied or stale resolver result is diagnostic evidence, never a
|
||||
# consumable capability token for a later mutation (#763).
|
||||
_clear_preflight_capability_state()
|
||||
else:
|
||||
# Stamp only after every resolver gate and fallible bookkeeping step
|
||||
# has completed. A denied, stale, malformed, or unexpectedly failing
|
||||
# attempt therefore cannot transiently authorize a later mutation.
|
||||
record_preflight_check(
|
||||
"capability", required_role, resolved_task=task
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user