Merge pull request 'Block role/tool namespace mismatch for author and reviewer mutations (Issue #209)' (#223) from feat/issue-209-namespace-mismatch into master
This commit was merged in pull request #223.
This commit is contained in:
+75
-4
@@ -257,6 +257,7 @@ import gitea_config # noqa: E402
|
||||
import capability_stop_terminal # noqa: E402
|
||||
import issue_duplicate_gate # noqa: E402
|
||||
import role_session_router # noqa: E402
|
||||
import role_namespace_gate # noqa: E402
|
||||
import task_capability_map # noqa: E402
|
||||
|
||||
|
||||
@@ -415,7 +416,8 @@ def _authenticated_username(host: str):
|
||||
|
||||
def _audit(action: str, *, host, remote, result, org=None, repo=None,
|
||||
reason=None, request_metadata=None, issue_number=None,
|
||||
pr_number=None, target_branch=None, head_sha=None, username=_UNSET):
|
||||
pr_number=None, target_branch=None, head_sha=None, username=_UNSET,
|
||||
mutation_task: str | None = None):
|
||||
"""Emit one audit record for a mutating action. No-op unless auditing is on.
|
||||
|
||||
Never raises — auditing must not break the action it records.
|
||||
@@ -426,6 +428,10 @@ def _audit(action: str, *, host, remote, result, org=None, repo=None,
|
||||
profile = get_profile()
|
||||
if username is _UNSET:
|
||||
username = _authenticated_username(host) if host else None
|
||||
ns_ctx = {}
|
||||
if mutation_task:
|
||||
ns_ctx = role_namespace_gate.mutation_audit_context(
|
||||
mutation_task, profile, remote=remote, repository=repo)
|
||||
event = gitea_audit.build_event(
|
||||
action=action,
|
||||
result=result,
|
||||
@@ -441,6 +447,9 @@ def _audit(action: str, *, host, remote, result, org=None, repo=None,
|
||||
head_sha=head_sha,
|
||||
reason=reason,
|
||||
request_metadata=request_metadata,
|
||||
mcp_namespace=ns_ctx.get("mcp_namespace"),
|
||||
task_role=ns_ctx.get("task_role"),
|
||||
operation=ns_ctx.get("operation"),
|
||||
)
|
||||
gitea_audit.write_event(event)
|
||||
except Exception:
|
||||
@@ -556,6 +565,10 @@ def gitea_create_issue(
|
||||
"number": None,
|
||||
"reasons": block_reasons,
|
||||
}
|
||||
blocked = _namespace_mutation_block(
|
||||
"create_issue", number=None, remote=remote)
|
||||
if blocked:
|
||||
return blocked
|
||||
blocked = _profile_permission_block(
|
||||
task_capability_map.required_permission("create_issue"),
|
||||
number=None,
|
||||
@@ -599,7 +612,7 @@ def gitea_create_issue(
|
||||
raise
|
||||
_audit("create_issue", host=h, remote=remote, org=o, repo=r,
|
||||
result=gitea_audit.SUCCEEDED, issue_number=data["number"],
|
||||
request_metadata={"title": title})
|
||||
request_metadata={"title": title}, mutation_task="create_issue")
|
||||
return _with_optional_url({"number": data["number"]}, data.get("html_url"))
|
||||
|
||||
|
||||
@@ -707,6 +720,25 @@ def gitea_create_pr(
|
||||
Returns:
|
||||
dict with 'number' of the created PR ('url' only with the reveal opt-in).
|
||||
"""
|
||||
ok, block_reasons = role_session_router.check_author_mutation_after_reviewer_stop(
|
||||
"create_pr")
|
||||
if not ok:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"number": None,
|
||||
"reasons": block_reasons,
|
||||
}
|
||||
blocked = _namespace_mutation_block(
|
||||
"create_pr", number=None, remote=remote)
|
||||
if blocked:
|
||||
return blocked
|
||||
blocked = _profile_permission_block(
|
||||
task_capability_map.required_permission("create_pr"),
|
||||
number=None,
|
||||
)
|
||||
if blocked:
|
||||
return blocked
|
||||
verify_preflight_purity(remote)
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
|
||||
@@ -753,11 +785,13 @@ def gitea_create_pr(
|
||||
except Exception as exc:
|
||||
_audit("create_pr", host=h, remote=remote, org=o, repo=r,
|
||||
result=gitea_audit.FAILED, reason=_redact(str(exc)),
|
||||
target_branch=head, request_metadata=meta)
|
||||
target_branch=head, request_metadata=meta,
|
||||
mutation_task="create_pr")
|
||||
raise
|
||||
_audit("create_pr", host=h, remote=remote, org=o, repo=r,
|
||||
result=gitea_audit.SUCCEEDED, pr_number=data["number"],
|
||||
target_branch=head, request_metadata=meta)
|
||||
target_branch=head, request_metadata=meta,
|
||||
mutation_task="create_pr")
|
||||
return _with_optional_url({"number": data["number"]}, data.get("html_url"))
|
||||
|
||||
|
||||
@@ -2728,6 +2762,43 @@ def _profile_permission_block(required_operation: str, **extra_fields) -> dict |
|
||||
return blocked
|
||||
|
||||
|
||||
def _namespace_mutation_block(mutation_task: str, **extra_fields) -> dict | None:
|
||||
"""Reviewer/author namespace alignment gate (#209)."""
|
||||
try:
|
||||
profile = get_profile()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"reasons": [
|
||||
f"profile could not be resolved (fail closed): {_redact(str(exc))}"],
|
||||
**extra_fields,
|
||||
}
|
||||
ok, reasons = role_namespace_gate.check_author_mutation_namespace(
|
||||
mutation_task, profile)
|
||||
if ok:
|
||||
return None
|
||||
blocked = {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"reasons": reasons,
|
||||
"namespace_block": True,
|
||||
"mcp_namespace": role_namespace_gate.infer_mcp_namespace(
|
||||
profile.get("profile_name")),
|
||||
**extra_fields,
|
||||
}
|
||||
_audit(
|
||||
mutation_task,
|
||||
host=None,
|
||||
remote=extra_fields.get("remote"),
|
||||
result=gitea_audit.BLOCKED,
|
||||
repo=extra_fields.get("repository"),
|
||||
reason="; ".join(reasons),
|
||||
mutation_task=mutation_task,
|
||||
)
|
||||
return blocked
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_issue_comments(
|
||||
issue_number: int,
|
||||
|
||||
Reference in New Issue
Block a user