"""Project-scoped Gitea connector with bounded PR-first writes.""" from __future__ import annotations import base64 import copy import difflib import hashlib import json import os import re import time from collections.abc import Callable from datetime import UTC, datetime from threading import Lock from typing import Any, Literal from urllib.parse import quote, unquote, urlparse import yaml # type: ignore[import-untyped] from packages.connector_kit import ( ConnectorContext, GiteaClient, GiteaError, GiteaRepositoryRegistry, PageRequest, RegisteredRepository, ResourcePublication, ToolPublication, decode_cursor, decode_file_payload, encode_cursor, is_secret_like, redact_sensitive_text, safe_content, safe_snippet, ) from packages.hub_policy import Governance, governed_tool from packages.hub_runtime.supervisor import runtime_snapshot from .changesets import ChangesetWorkspace, apply_unified_patch, validate_operations from .lifecycle import cleanup_basis from .managed_validation import ( MANAGED_VALIDATION_CONTEXT, MANAGED_VALIDATION_DIGEST, MANAGED_VALIDATION_PATH, MANAGED_VALIDATION_WORKFLOW, ) _SAFE_REF = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$") _SAFE_CHANGE_BRANCH = _SAFE_REF _SAFE_REPOSITORY_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$") _SAFE_ACCOUNT_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$") _SHA = re.compile(r"^[0-9a-fA-F]{40,64}$") _ORCHESTRATION_SUFFIX = re.compile(r"^[a-z0-9][a-z0-9._-]{0,79}$") _WORKSPACE_PROFILES = ("test", "lint", "typecheck", "build", "security", "full") _GOVERNANCE_BATCH_LIMIT = 5 _FAILED_CONCLUSIONS = frozenset({"failure", "failed", "cancelled", "timed_out", "timed-out"}) _FAILURE_LINE = re.compile( r"(?i)(error|failed|failure|fatal|traceback|exception|" r"exit(?:ed)?(?:\s+with)?(?:\s+code)?\s*[=: ]\s*\d+)" ) _EXIT_CODE = re.compile(r"(?i)exit(?:ed)?(?:\s+with)?(?:\s+code)?\s*[=: ]\s*(\d{1,3})") _FAILURE_MAX_JOBS = 20 _FAILURE_MAX_LINES = 100 _FAILURE_MAX_BYTES = 180_000 _VALIDATION_REUSE_SECONDS = 2 * 60 * 60 _DISCOVERY_CACHE_SECONDS = 5 * 60 _CODE_SEARCH_MAX_FILES = 40 _CODE_SEARCH_BUDGET_SECONDS = 4.0 _CODE_SEARCH_BLOB_TIMEOUT_SECONDS = 0.75 _REPOSITORY_CONTEXT_TREE_LIMIT = 64 _REPOSITORY_CONTEXT_DOCUMENT_LIMIT = 4 _REPOSITORY_CONTEXT_DOCUMENT_BYTES = 4_096 _REPOSITORY_CONTEXT_DOCUMENT_TOTAL_BYTES = 8_192 _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY = "jens/projectbrain" _PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT = "Pull request / quality (pull_request)" _PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS = ( MANAGED_VALIDATION_CONTEXT, _PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT, ) _PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS = { "enable_push": False, "enable_push_whitelist": False, "enable_force_push": False, "enable_force_push_allowlist": False, "enable_bypass_allowlist": False, "enable_status_check": True, "block_admin_merge_override": True, } # Original source commit for the ProjectBrain-specific profile lineage. _PROJECTBRAIN_VALIDATION_PROFILE_SOURCE_SHA = "57157b1b24d3679018b54970c934fc35d1735df7" # SHA-256 of JSON-dumped jobs.full (sort_keys, compact separators), parsed with # BaseLoader. It pins the reviewed ProjectBrain workflow copy below. # Frozen workflow source and copied fixture raw SHA-256: # 2f78d37ff51f38f0a3b0c9e222eb2dbf83d5108aee3615eb53f2c01283f37f34. # Refresh only after review. _PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST = ( "a3fefb8865f0bd0d4a8c10af79eeb5e22367c5e82e9333a2fe04bdaf97d0193b" ) _PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS = 200_000 _PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS = 10_000 _PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH = 64 _PROJECTBRAIN_VALIDATION_RUN_NAME = "Managed validation / ${{ inputs.profile || 'full' }}" _PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS = [ "test", "lint", "typecheck", "build", "security", "full", ] _PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS = frozenset( {"name", "run-name", "on", "permissions", "concurrency", "jobs"} ) _PROJECTBRAIN_VALIDATION_CONCURRENCY = { "group": "managed-validation-${{ gitea.repository }}-${{ gitea.ref }}", "cancel-in-progress": "true", } class _UniqueWorkflowLoader(yaml.BaseLoader): # type: ignore[misc] """Parse workflow YAML without YAML 1.1's special treatment of the `on` key.""" def _construct_unique_workflow_mapping(loader: Any, node: Any) -> dict[str, Any]: mapping: dict[str, Any] = {} for key_node, value_node in node.value: key = loader.construct_object(key_node) if not isinstance(key, str) or key in mapping: raise yaml.constructor.ConstructorError( "while constructing a workflow mapping", node.start_mark, "workflow mapping keys must be unique strings", key_node.start_mark, ) mapping[key] = loader.construct_object(value_node) return mapping _UniqueWorkflowLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_unique_workflow_mapping, ) def _workflow_mapping(value: Any) -> dict[str, Any] | None: return value if isinstance(value, dict) else None def _workflow_yaml_is_bounded_and_alias_free(workflow_content: str) -> bool: if len(workflow_content) > _PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS: return False container_starts = ( yaml.tokens.BlockMappingStartToken, yaml.tokens.BlockSequenceStartToken, yaml.tokens.FlowMappingStartToken, yaml.tokens.FlowSequenceStartToken, ) container_ends = ( yaml.tokens.BlockEndToken, yaml.tokens.FlowMappingEndToken, yaml.tokens.FlowSequenceEndToken, ) depth = 0 try: for token_count, token in enumerate( yaml.scan(workflow_content, Loader=_UniqueWorkflowLoader), start=1 ): if token_count > _PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS: return False if isinstance(token, yaml.tokens.AnchorToken | yaml.tokens.AliasToken): return False if isinstance(token, container_starts): depth += 1 if depth > _PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH: return False elif isinstance(token, container_ends): depth -= 1 if depth < 0: return False except (yaml.YAMLError, RecursionError, TypeError, ValueError): return False return depth == 0 def _projectbrain_managed_validation_is_strong(workflow_content: str) -> bool: if not _workflow_yaml_is_bounded_and_alias_free(workflow_content): return False try: # BaseLoader constructs only strings/lists/maps; it never creates Python objects. workflow = yaml.load(workflow_content, Loader=_UniqueWorkflowLoader) # noqa: S506 except (yaml.YAMLError, RecursionError, TypeError, ValueError): return False document = _workflow_mapping(workflow) if document is None or document.get("name") != "Managed validation": return False if ( not set(document).issubset(_PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS) or document.get("run-name") != _PROJECTBRAIN_VALIDATION_RUN_NAME or document.get("permissions") != {"contents": "read"} or document.get("concurrency") != _PROJECTBRAIN_VALIDATION_CONCURRENCY ): return False triggers = _workflow_mapping(document.get("on")) if triggers is None or set(triggers) != {"push", "pull_request", "workflow_dispatch"}: return False dispatch = _workflow_mapping(triggers.get("workflow_dispatch")) if dispatch is None or set(dispatch) != {"inputs"}: return False dispatch_inputs = _workflow_mapping(dispatch.get("inputs")) profile_input = ( _workflow_mapping(dispatch_inputs.get("profile")) if dispatch_inputs is not None else None ) if ( dispatch_inputs is None or set(dispatch_inputs) != {"profile"} or profile_input is None or set(profile_input) != {"description", "required", "default", "type", "options"} or not isinstance(profile_input.get("description"), str) or profile_input.get("required") != "true" or profile_input.get("default") != "full" or profile_input.get("type") != "choice" or profile_input.get("options") != _PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS ): return False push = _workflow_mapping(triggers.get("push")) if push is None or set(push) != {"branches"} or push.get("branches") != ["main"]: return False pull_request = triggers.get("pull_request") if pull_request not in (None, ""): pull_request_mapping = _workflow_mapping(pull_request) if pull_request_mapping is None: return False if set(pull_request_mapping) - {"branches"}: return False branches = pull_request_mapping.get("branches") if branches is not None and branches != ["main"]: return False jobs = _workflow_mapping(document.get("jobs")) if jobs is None or set(jobs) != {"full"}: return False full_job = _workflow_mapping(jobs.get("full")) if full_job is None: return False try: full_job_digest = hashlib.sha256( json.dumps(full_job, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest() except (RecursionError, TypeError, ValueError): return False return full_job_digest == _PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST def _managed_validation_profile( repository_qualified_name: str, workflow_content: str | None ) -> str | None: """Accept only the canonical workflow, except for ProjectBrain's stronger bounded profile.""" if workflow_content is None: return None if repository_qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY: if _projectbrain_managed_validation_is_strong(workflow_content): return "projectbrain" return None digest = hashlib.sha256(workflow_content.encode("utf-8")).hexdigest() return "canonical" if digest == MANAGED_VALIDATION_DIGEST else None def _select_validation_workflow(workflows: list[dict[str, Any]]) -> dict[str, Any] | None: """Prefer the bounded managed contract over legacy validation workflows.""" managed: list[dict[str, Any]] = [] legacy: list[dict[str, Any]] = [] for item in workflows: path = str(item.get("path", "")).casefold() name = str(item.get("name", "")).casefold() if path.endswith("managed-validation.yml") or name == "managed validation": managed.append(item) elif path.endswith("chatgpt-validation.yml") or name == "chatgpt validation": legacy.append(item) candidates = managed or legacy return ( min(candidates, key=lambda item: str(item.get("path", item.get("id", "")))) if candidates else None ) def _validation_polling( latest_run: dict[str, Any] | None, expected_head_sha: str ) -> dict[str, Any]: if latest_run is None: state, terminal, poll_after_ms, next_action = "not-started", True, 0, "run-validation" elif expected_head_sha and latest_run.get("headSha") != expected_head_sha: state, terminal, poll_after_ms, next_action = "stale", True, 0, "run-validation" else: status = str(latest_run.get("status", "")).casefold() conclusion = str(latest_run.get("conclusion", "")).casefold() if status == "completed": state = "succeeded" if conclusion == "success" else "failed" terminal, poll_after_ms = True, 0 next_action = "continue" if state == "succeeded" else "inspect-or-retry-validation" elif status in {"queued", "pending", "waiting", "requested"}: state, terminal, poll_after_ms, next_action = "queued", False, 5_000, "wait-for-change" else: state, terminal, poll_after_ms, next_action = "running", False, 4_000, "wait-for-change" version_input = { "expectedHeadSha": expected_head_sha, "id": latest_run.get("id") if latest_run else None, "headSha": latest_run.get("headSha") if latest_run else None, "status": latest_run.get("status") if latest_run else None, "conclusion": latest_run.get("conclusion") if latest_run else None, "updatedAt": latest_run.get("updatedAt") if latest_run else None, } return { "validationState": state, "terminal": terminal, "pollAfterMs": poll_after_ms, "nextAction": next_action, "validationStateVersion": hashlib.sha256( json.dumps(version_input, sort_keys=True, separators=(",", ":")).encode() ).hexdigest()[:24], } def _desired_branch_protection(default_branch: str) -> dict[str, Any]: return { "rule_name": default_branch, "branch_name": default_branch, "priority": 1, "enable_push": False, "enable_push_whitelist": False, "push_whitelist_usernames": [], "push_whitelist_teams": [], "push_whitelist_deploy_keys": False, "enable_force_push": False, "enable_force_push_allowlist": False, "force_push_allowlist_usernames": [], "force_push_allowlist_teams": [], "force_push_allowlist_deploy_keys": False, "enable_merge_whitelist": False, "merge_whitelist_usernames": [], "merge_whitelist_teams": [], "enable_bypass_allowlist": False, "bypass_allowlist_usernames": [], "bypass_allowlist_teams": [], "enable_status_check": True, "status_check_contexts": [MANAGED_VALIDATION_CONTEXT], "required_approvals": 0, "enable_approvals_whitelist": False, "approvals_whitelist_username": [], "approvals_whitelist_teams": [], "block_on_rejected_reviews": True, "block_on_official_review_requests": False, "block_on_outdated_branch": True, "dismiss_stale_approvals": True, "ignore_stale_approvals": False, "require_signed_commits": False, "protected_file_patterns": "", "unprotected_file_patterns": "", "block_admin_merge_override": True, } def _branch_protection_projection(payload: dict[str, Any] | None) -> dict[str, Any] | None: if payload is None: return None desired_keys = _desired_branch_protection( str(payload.get("rule_name") or payload.get("branch_name") or "main") ) return {key: payload.get(key) for key in desired_keys} def _branch_protection_effective_projection( payload: dict[str, Any] | None, ) -> dict[str, Any] | None: if payload is None: return None volatile_metadata = {"id", "created_at", "updated_at"} return { key: value for key, value in payload.items() if key not in volatile_metadata and key != "status_check_contexts" } def _validate_ref(value: str, *, change_branch: bool = False) -> str: pattern = _SAFE_CHANGE_BRANCH if change_branch else _SAFE_REF if ( not pattern.fullmatch(value) or ".." in value or "//" in value or value.endswith(("/", ".", ".lock")) ): raise GiteaError("GITEA_TARGET_INVALID", "Gitea ref is outside the safe write policy") return value def _validate_work_branch(value: str, default_branch: str) -> str: """Accept any valid Git branch except the repository default branch.""" branch = _validate_ref(value, change_branch=True) if not default_branch: raise GiteaError("GITEA_REPOSITORY_NOT_READY", "repository has no default branch") if branch == default_branch: raise GiteaError( "GITEA_TARGET_INVALID", "direct writes to the repository default branch are not allowed; use a pull request", ) return branch def _validate_file_path(path: str) -> tuple[str, ...]: if not 1 <= len(path) <= 2048 or path.startswith(("/", "\\")): raise GiteaError("GITEA_PATH_INVALID", "repository file path is invalid") parts = tuple(path.split("/")) if any( not part or part in {".", ".."} or "\\" in part or any(ord(char) < 0x20 for char in part) for part in parts ): raise GiteaError("GITEA_PATH_INVALID", "repository file path is invalid") return parts def _validate_text(value: str, *, field: str, minimum: int, maximum: int) -> str: encoded = value.encode("utf-8") if not minimum <= len(encoded) <= maximum or "\x00" in value: raise GiteaError("GITEA_INPUT_INVALID", f"{field} is outside the connector budget") return value class GiteaConnector: """Expose only repositories registered for the caller's project.""" connector_type = "gitea" version = "1.14.0" def __init__( self, client: GiteaClient, registry: GiteaRepositoryRegistry, governance: Governance | None = None, *, write_client: GiteaClient | None = None, write_owner: str | None = None, repository_create_project: tuple[Any, str] | None = None, changeset_workspace: ChangesetWorkspace | None = None, discovery_projects: dict[Any, str] | None = None, ) -> None: self.client = client self.write_client = write_client if write_owner is not None and not _SAFE_ACCOUNT_NAME.fullmatch(write_owner): raise ValueError("Gitea write owner is invalid") self.write_owner = write_owner self.repository_create_project = repository_create_project self.changeset_workspace = changeset_workspace self.registry = registry self.governance = governance self.discovery_projects = discovery_projects or {} self._last_discovery: dict[Any, float] = {} self._discovery_lock = Lock() self._tree_cache: dict[tuple[str, str, int], tuple[float, dict[str, Any]]] = {} self._tree_cache_lock = Lock() self._readiness_cache: tuple[float, dict[str, Any]] | None = None self._readiness_lock = Lock() self._validation_dispatches: dict[tuple[str, str, str, str], tuple[float, int]] = {} self._validation_dispatch_lock = Lock() def _git_tree_cached( self, repository: RegisteredRepository, ref: str, *, page: int = 1, per_page: int = 1_000, ) -> tuple[dict[str, Any], bool]: key = (repository.qualified_name, ref, page) now = time.monotonic() with self._tree_cache_lock: cached = self._tree_cache.get(key) if cached is not None and now - cached[0] < 30: return copy.deepcopy(cached[1]), True payload = self.client.git_tree( repository, ref, recursive=True, page=page, per_page=per_page ) with self._tree_cache_lock: self._tree_cache[key] = (now, copy.deepcopy(payload)) if len(self._tree_cache) > 64: oldest = min(self._tree_cache, key=lambda item: self._tree_cache[item][0]) self._tree_cache.pop(oldest, None) return payload, False def _invalidate_repository_cache(self, repository: RegisteredRepository) -> None: with self._tree_cache_lock: for key in tuple(self._tree_cache): if key[0] == repository.qualified_name: self._tree_cache.pop(key, None) def _refresh_discovered(self, project_id: Any) -> None: project_slug = self.discovery_projects.get(project_id) if project_slug is None: return with self._discovery_lock: now = time.monotonic() if now - self._last_discovery.get(project_id, 0.0) < _DISCOVERY_CACHE_SECONDS: return discovered: list[dict[str, Any]] = [] seen: set[str] = set() identity_client = self.write_client or self.client loaders = ( (self.client.accessible_private_repositories, False), (identity_client.authenticated_user_repositories, True), ) for loader, optional in loaders: for page in range(1, 101): try: items = loader(page, 100) except GiteaError as error: if optional and error.code in { "GITEA_ACCESS_DENIED", "GITEA_NOT_FOUND", "GITEA_UNSUPPORTED", }: break raise for item in items: full_name = str(item.get("full_name", "")).casefold() if full_name and full_name not in seen: seen.add(full_name) discovered.append(item) if len(discovered) > 10_000: raise GiteaError( "GITEA_DISCOVERY_LIMIT", "repository discovery exceeded 10000 items", ) if len(items) < 100: break else: raise GiteaError( "GITEA_DISCOVERY_LIMIT", "repository discovery exceeded 100 pages" ) self.registry.add_discovered(project_id, project_slug, discovered) self._last_discovery[project_id] = now def _writer(self) -> GiteaClient: if self.write_client is None: raise GiteaError( "GITEA_WRITES_DISABLED", "Gitea write capability is not enabled for this runtime" ) return self.write_client def _repository_write_owner(self, context: ConnectorContext) -> str: """Resolve the fixed create namespace without broadening the token's read scope.""" if self.write_owner is not None: return self.write_owner project_id, _ = self.repository_create_project or ( context.project_id, context.project_slug, ) owners = {item.owner for item in self.registry.list(project_id)} if len(owners) == 1: return next(iter(owners)) raise GiteaError( "GITEA_CONFIGURATION_INVALID", "Gitea repository creation requires one configured write owner", ) def _envelope( self, context: ConnectorContext, data: Any, *, source: str = "gitea", truncated: bool = False, next_cursor: str | None = None, warnings: tuple[str, ...] = (), ) -> dict[str, Any]: return { "data": data, "meta": { "project": context.project_slug, "environment": context.environment, "source": source, "truncated": truncated, "nextCursor": next_cursor, "generatedAt": datetime.now(UTC).isoformat(), }, "warnings": list(warnings), } @staticmethod def _snapshot_id(value: Any) -> str: return hashlib.sha256( json.dumps(value, sort_keys=True, separators=(",", ":")).encode() ).hexdigest()[:24] @staticmethod def _compact_comparison( payload: dict[str, Any], *, base: str, head: str, reverse_payload: dict[str, Any] | None = None, ) -> dict[str, Any]: raw_commits = payload.get("commits") raw_files = payload.get("files") commits = raw_commits if isinstance(raw_commits, list) else [] files = raw_files if isinstance(raw_files, list) else [] truncated = len(commits) > 50 or len(files) > 100 return { "base": base, "head": head, "aheadBy": max(int(payload.get("ahead_by", payload.get("total_commits", 0)) or 0), 0), "behindBy": max( int( payload.get( "behind_by", (reverse_payload or {}).get("total_commits", 0), ) or 0 ), 0, ), "totalCommits": max(int(payload.get("total_commits", len(commits)) or 0), 0), "commits": [ { "sha": str(item.get("sha", item.get("id", "")))[:64], "message": redact_sensitive_text( str((item.get("commit") or {}).get("message", item.get("message", ""))), max_bytes=1_000, ), } for item in commits[:50] if isinstance(item, dict) ], "files": [ { "path": str(item.get("filename", ""))[:2_048], "status": str(item.get("status", ""))[:32], } for item in files[:100] if isinstance(item, dict) ], "truncated": truncated, } @staticmethod def _failure_excerpt(logs: str) -> tuple[list[str], int | None, str | None, str]: lines = logs.splitlines() matches = [index for index, line in enumerate(lines) if _FAILURE_LINE.search(line)] selected: list[int] = [] for index in matches: selected.extend(range(max(index - 3, 0), min(index + 4, len(lines)))) selected = sorted(set(selected))[-100:] excerpt = [redact_sensitive_text(lines[index], max_bytes=2_000) for index in selected] exit_code = None for line in reversed(excerpt): match = _EXIT_CODE.search(line) if match is not None: exit_code = int(match.group(1)) break command = next( ( redact_sensitive_text(line.strip(), max_bytes=1_000) for line in reversed(lines[: (matches[-1] + 1 if matches else len(lines))]) if line.strip().startswith(("Run ", "$ ", "+ ")) ), None, ) error_code = "VALIDATION_COMMAND_FAILED" if exit_code is not None else "VALIDATION_FAILED" return excerpt, exit_code, command, error_code def _repo(self, context: ConnectorContext, repository_ref: str) -> RegisteredRepository: self._refresh_discovered(context.project_id) return self.registry.resolve(context.project_id, repository_ref) @staticmethod def _page(page: PageRequest | None) -> tuple[int, int, str | None]: request = page or PageRequest(limit=25) cursor = decode_cursor(request.cursor, request.limit.__str__()) return request.limit, cursor.offset, request.cursor def _collection( self, context: ConnectorContext, repository_ref: str, page: PageRequest | None, loader: Any, mapper: Any, kind: str, ) -> dict[str, Any]: repository = self._repo(context, repository_ref) request = page or PageRequest(limit=25) cursor = decode_cursor( request.cursor, str(context.project_id), repository_ref, kind, str(request.limit) ) page_number = cursor.offset // request.limit + 1 items = mapper(loader(repository, page_number, request.limit), repository) next_cursor = None if len(items) == request.limit: next_cursor = encode_cursor( cursor.offset + len(items), str(context.project_id), repository_ref, kind, str(request.limit), ) data = {"items": items, "nextCursor": next_cursor, "truncated": next_cursor is not None} return self._envelope( context, data, source=f"gitea:{repository_ref}", truncated=next_cursor is not None, next_cursor=next_cursor, ) @governed_tool("gitea.repository.list") def repository_list( self, context: ConnectorContext, *, page: PageRequest | None = None ) -> dict[str, Any]: self._refresh_discovered(context.project_id) request = page or PageRequest(limit=25) cursor = decode_cursor( request.cursor, str(context.project_id), "repositories", str(request.limit) ) repositories = self.registry.list(context.project_id) selected = repositories[cursor.offset : cursor.offset + request.limit] items = [] for item in selected: payload = self.client.repository(item) default_branch = payload.get("default_branch") if not payload.get("empty", False) and not ( isinstance(default_branch, str) and default_branch.strip() ): raise GiteaError( "GITEA_INVALID_RESPONSE", "Gitea returned a non-empty repository without a default branch", ) items.append( { "repositoryRef": item.repository_ref, "name": str(payload.get("name", item.name))[:120], "owner": str((payload.get("owner") or {}).get("login", item.owner))[:120], "private": bool(payload.get("private", item.private)), "defaultBranch": ( default_branch[:300] if isinstance(default_branch, str) and default_branch.strip() else None ), "sourceUri": item.source_uri, } ) next_cursor = None if cursor.offset + len(items) < len(repositories): next_cursor = encode_cursor( cursor.offset + len(items), str(context.project_id), "repositories", str(request.limit), ) data = {"items": items, "nextCursor": next_cursor, "truncated": next_cursor is not None} return self._envelope( context, data, source="gitea:registry", truncated=next_cursor is not None, next_cursor=next_cursor, ) @governed_tool("gitea.repository.get") def repository_get(self, context: ConnectorContext, repository_ref: str) -> dict[str, Any]: repository = self._repo(context, repository_ref) payload = self.client.repository(repository) owner_value = payload.get("owner") owner = owner_value if isinstance(owner_value, dict) else {} data = { "repositoryRef": repository.repository_ref, "name": str(payload.get("name", repository.name))[:120], "owner": str(owner.get("login", repository.owner))[:120], "fullName": str(payload.get("full_name", repository.qualified_name))[:241], "description": safe_snippet(str(payload.get("description", "")), max_bytes=2000), "private": bool(payload.get("private", repository.private)), "defaultBranch": str(payload.get("default_branch", ""))[:300], "sourceUri": repository.source_uri, "updatedAt": str(payload.get("updated_at", ""))[:64], } return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.repository.context") def repository_context( self, context: ConnectorContext, repository_ref: str, ref: str | None = None ) -> dict[str, Any]: repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) selected_ref = _validate_ref(ref) if ref else str(metadata.get("default_branch", "")) if not selected_ref: raise GiteaError("GITEA_TARGET_INVALID", "repository has no usable default branch") tree_payload, cache_hit = self._git_tree_cached(repository, selected_ref) raw_tree = tree_payload.get("tree") tree = raw_tree if isinstance(raw_tree, list) else [] files = [item for item in tree if isinstance(item, dict) and item.get("type") == "blob"] important_names = { "AGENTS.md", "CLAUDE.md", "README.md", "README_NL.md", "pyproject.toml", "package.json", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "Dockerfile", "docker-compose.yml", "docker-compose.yaml", } important_paths = [ str(item.get("path", "")) for item in files if str(item.get("path", "")).split("/")[-1] in important_names ] documents: list[dict[str, Any]] = [] remaining_document_bytes = _REPOSITORY_CONTEXT_DOCUMENT_TOTAL_BYTES for path in important_paths[:_REPOSITORY_CONTEXT_DOCUMENT_LIMIT]: if remaining_document_bytes <= 0: break payload = self.client.file(repository, _validate_file_path(path), selected_ref) document_limit = min(_REPOSITORY_CONTEXT_DOCUMENT_BYTES, remaining_document_bytes) content, truncated, sha = decode_file_payload(payload, document_limit) filtered = safe_content(content) emitted_bytes = len((filtered or "").encode("utf-8")) remaining_document_bytes = max(remaining_document_bytes - emitted_bytes, 0) documents.append( { "path": path, "sha": sha[:64], "content": filtered or "", "excluded": filtered is None, "truncated": truncated, } ) path_set = {str(item.get("path", "")) for item in files} stacks = [] for marker, label in ( ("pyproject.toml", "python"), ("package.json", "node"), ("Cargo.toml", "rust"), ("go.mod", "go"), ("pom.xml", "java-maven"), ("Dockerfile", "container"), ): if marker in path_set: stacks.append(label) tree_truncated = ( bool(tree_payload.get("truncated", False)) or len(tree) > _REPOSITORY_CONTEXT_TREE_LIMIT ) documents_truncated = len(important_paths) > len(documents) or any( bool(item["truncated"]) for item in documents ) context_truncated = tree_truncated or documents_truncated data = { "repositoryRef": repository_ref, "fullName": repository.qualified_name, "ref": selected_ref, "defaultBranch": str(metadata.get("default_branch", ""))[:300], "description": safe_snippet(str(metadata.get("description", "")), max_bytes=2_000), "stacks": stacks, "tree": [ { "path": str(item.get("path", ""))[:2_048], "type": str(item.get("type", ""))[:16], "sha": str(item.get("sha", ""))[:64], "size": max(int(item.get("size", 0) or 0), 0), } for item in tree[:_REPOSITORY_CONTEXT_TREE_LIMIT] if isinstance(item, dict) ], "documents": documents, "treeTruncated": tree_truncated, "documentsTruncated": documents_truncated, } result = self._envelope( context, data, source=f"gitea:{repository_ref}", truncated=context_truncated, ) result["meta"]["cacheHit"] = cache_hit result["meta"]["cacheTtlSeconds"] = 30 return result @governed_tool("gitea.repository.health") def repository_health(self, context: ConnectorContext, repository_ref: str) -> dict[str, Any]: repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) branches = self.client.branches(repository, 1, 100) pulls = self.client.pull_requests(repository, 1, 100) warnings: list[str] = [] try: workflows = self.client.action_workflows(repository) except GiteaError as exc: if exc.code not in { "GITEA_NOT_FOUND", "GITEA_UNSUPPORTED", "GITEA_REQUEST_FAILED", "GITEA_INVALID_RESPONSE", }: raise workflows = [] warnings.append("Gitea Actions workflow discovery is unavailable for this repository.") default_branch = str(metadata.get("default_branch", ""))[:300] default_payload = next( (item for item in branches if str(item.get("name", "")) == default_branch), None ) change_branches = [ str(item.get("name", ""))[:300] for item in branches if str(item.get("name", "")) != default_branch and _SAFE_CHANGE_BRANCH.fullmatch(str(item.get("name", ""))) ] open_pulls = [item for item in pulls if str(item.get("state", "")) == "open"] source_revision = str(((default_payload or {}).get("commit") or {}).get("id", ""))[:64] snapshot_material = { "repositoryRef": repository_ref, "sourceRevision": source_revision, "branches": [ { "name": str(item.get("name", ""))[:300], "protected": bool(item.get("protected", False)), } for item in branches ], "pullRequests": [ { "number": max(int(item.get("number", 0) or 0), 0), "state": str(item.get("state", ""))[:32], } for item in pulls ], } snapshot_id = hashlib.sha256( json.dumps(snapshot_material, sort_keys=True, separators=(",", ":")).encode() ).hexdigest()[:24] validation_workflow = any( str(item.get("path", "")).endswith("chatgpt-validation.yml") or str(item.get("path", "")).endswith("managed-validation.yml") or str(item.get("name", "")).casefold() == "chatgpt validation" or str(item.get("name", "")).casefold() == "managed validation" for item in workflows ) findings = [] if default_payload is None: findings.append("default-branch-not-listed") elif not bool(default_payload.get("protected", False)): findings.append("default-branch-unprotected") if not validation_workflow: findings.append("validation-workflow-missing") if len(branches) >= 100: findings.append("branch-inventory-truncated") if len(pulls) >= 100: findings.append("pull-request-inventory-truncated") return self._envelope( context, { "repositoryRef": repository_ref, "defaultBranch": default_branch, "defaultBranchProtected": bool( isinstance(default_payload, dict) and default_payload.get("protected", False) ), "branchCount": len(branches), "changeBranches": change_branches, "safeBranchCount": len(change_branches), "openPullRequests": [self._pull_request_projection(item) for item in open_pulls], "openPullRequestCount": len(open_pulls), "validationWorkflowAvailable": validation_workflow, "findings": findings, "healthy": not findings, "snapshotId": snapshot_id, "sourceRevision": source_revision, }, source=f"gitea:{repository_ref}", truncated=len(branches) >= 100 or len(pulls) >= 100, warnings=tuple(warnings), ) @staticmethod def _public_governance_plan(plan: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in plan.items() if not key.startswith("_")} def _governance_plan_data( self, context: ConnectorContext, repository_ref: str ) -> dict[str, Any]: repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) default_branch = str(metadata.get("default_branch", ""))[:300] blockers: list[str] = [] if bool(metadata.get("empty", False)) or not default_branch: blockers.append("repository-empty") protections = self.client.branch_protections(repository) current = next( ( item for item in protections if str(item.get("rule_name") or item.get("branch_name")) == default_branch ), None, ) desired = _desired_branch_protection(default_branch or "main") required_contexts = ( _PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS if repository.qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY else (MANAGED_VALIDATION_CONTEXT,) ) existing_contexts = current.get("status_check_contexts") if current is not None else None preserved_contexts = ( {str(item) for item in existing_contexts if str(item).strip()} if isinstance(existing_contexts, list) else set() ) desired["status_check_contexts"] = sorted(set(required_contexts).union(preserved_contexts)) current_projection = _branch_protection_projection(current) protection_diff = { key: { "current": None if current_projection is None else current_projection.get(key), "desired": value, } for key, value in desired.items() if current_projection is None or current_projection.get(key) != value } workflow_content: str | None = None workflow_sha = "" if default_branch and "repository-empty" not in blockers: try: workflow_payload = self.client.file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), default_branch ) workflow_content, workflow_truncated, workflow_sha = decode_file_payload( workflow_payload, 200_000 ) if workflow_truncated: blockers.append("managed-validation-workflow-too-large") except GiteaError as exc: if exc.code != "GITEA_NOT_FOUND": raise workflow_digest = ( hashlib.sha256(workflow_content.encode("utf-8")).hexdigest() if workflow_content is not None else None ) workflow_profile = _managed_validation_profile(repository.qualified_name, workflow_content) is_projectbrain = ( repository.qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY ) if is_projectbrain and workflow_profile is None: blockers.append("managed-validation-projectbrain-profile-invalid") # Fail closed: governance must never replace ProjectBrain's # repository-specific database/E2E gate with the generic baseline. workflow_action = "none" desired_workflow_digest = None else: workflow_action = ( "create" if workflow_content is None else "none" if workflow_profile is not None else "update" ) desired_workflow_digest = ( workflow_digest if workflow_profile == "projectbrain" else MANAGED_VALIDATION_DIGEST ) if is_projectbrain and current_projection is not None: if any( current_projection.get(key) != expected for key, expected in _PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS.items() ): blockers.append("managed-validation-projectbrain-protection-safety-locks-invalid") if workflow_profile == "projectbrain": desired = dict(current_projection) desired["status_check_contexts"] = sorted( set(required_contexts).union(preserved_contexts) ) protection_diff = { key: { "current": None if current_projection is None else current_projection.get(key), "desired": value, } for key, value in desired.items() if current_projection is None or current_projection.get(key) != value } protection_action = "create" if current is None else "update" if protection_diff else "none" plan_payload = { "repositoryRef": repository_ref, "defaultBranch": default_branch, "workflow": { "path": MANAGED_VALIDATION_PATH, "currentDigest": workflow_digest, "desiredDigest": desired_workflow_digest, "profile": workflow_profile or ("projectbrain-required" if is_projectbrain else "canonical"), "action": workflow_action, }, "branchProtection": { "current": current_projection, "desired": desired, "diff": protection_diff, "action": protection_action, }, "blockers": blockers, } effective_protection = _branch_protection_effective_projection(current) plan_hash_source: dict[str, Any] = plan_payload if is_projectbrain: plan_hash_source = { "plan": plan_payload, "effectiveProtection": effective_protection, } plan_hash = hashlib.sha256( json.dumps(plan_hash_source, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() protection_only_available = ( is_projectbrain and current is not None and current.get("enable_status_check") is True and default_branch == "main" and isinstance(desired.get("status_check_contexts"), list) and set(blockers) == {"managed-validation-projectbrain-profile-invalid"} ) protection_only_plan_hash = ( hashlib.sha256( json.dumps( { "applyScope": "protection_only", "plan": plan_payload, "effectiveProtection": effective_protection, }, sort_keys=True, separators=(",", ":"), ).encode() ).hexdigest() if protection_only_available else None ) return { **plan_payload, "planHash": plan_hash, "protectionOnlyPlanHash": protection_only_plan_hash, "protectionOnlyAvailable": protection_only_available, "compliant": not blockers and workflow_action == "none" and protection_action == "none", "_repository": repository, "_workflowContent": workflow_content, "_workflowSha": workflow_sha, "_currentProtection": current, "_protectionOnlyAvailable": protection_only_available, } @governed_tool("gitea.branchprotection.get") def branch_protection_get( self, context: ConnectorContext, repository_ref: str ) -> dict[str, Any]: plan = self._governance_plan_data(context, repository_ref) return self._envelope( context, { "repositoryRef": repository_ref, "defaultBranch": plan["defaultBranch"], "protection": plan["branchProtection"]["current"], }, source=f"gitea:{repository_ref}", ) @governed_tool("gitea.branchprotection.preview") def branch_protection_preview( self, context: ConnectorContext, repository_ref: str ) -> dict[str, Any]: plan = self._governance_plan_data(context, repository_ref) return self._envelope( context, self._public_governance_plan(plan), source=f"gitea:{repository_ref}", ) def _write_managed_validation_workflow( self, context: ConnectorContext, repository: RegisteredRepository, default_branch: str, workflow_action: str, workflow_sha: str, current_protection: dict[str, Any] | None, ) -> bool: writer = self._writer() def write_workflow() -> None: if workflow_action == "create": writer.create_file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), branch=default_branch, content=MANAGED_VALIDATION_WORKFLOW, message="ci: add managed validation contract [skip ci]", ) elif workflow_action == "update": writer.update_file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), branch=default_branch, content=MANAGED_VALIDATION_WORKFLOW, message="ci: align managed validation contract [skip ci]", sha=workflow_sha, ) try: write_workflow() return False except GiteaError as exc: if exc.code == "GITEA_ACCESS_DENIED" and current_protection is not None: raise GiteaError( "GITEA_GOVERNANCE_BLOCKED", "protected workflow changes require a validated pull request", ) from exc raise @governed_tool("gitea.branchprotection.ensure") def branch_protection_ensure( self, context: ConnectorContext, repository_ref: str, expected_plan_hash: str, apply_scope: Literal["all", "protection_only"] = "all", ) -> dict[str, Any]: if apply_scope not in {"all", "protection_only"}: raise GiteaError("GITEA_INPUT_INVALID", "governance apply scope is not allowlisted") plan = self._governance_plan_data(context, repository_ref) expected_hash = plan["planHash"] if apply_scope == "all" else plan["protectionOnlyPlanHash"] if ( not isinstance(expected_hash, str) or not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash) or expected_plan_hash != expected_hash ): raise GiteaError( "GITEA_PLAN_STALE", "governance state changed; preview the repository again before applying", ) if apply_scope == "protection_only": allowed_blockers = {"managed-validation-projectbrain-profile-invalid"} if not plan["_protectionOnlyAvailable"] or any( blocker not in allowed_blockers for blocker in plan["blockers"] ): raise GiteaError( "GITEA_GOVERNANCE_BLOCKED", "ProjectBrain protection-only apply requires an existing " "enabled status-check rule", ) current_protection = plan["_currentProtection"] if current_protection is None: raise GiteaError( "GITEA_GOVERNANCE_BLOCKED", "ProjectBrain protection-only apply requires an existing " "enabled status-check rule", ) current_contexts = current_protection.get("status_check_contexts") expected_contexts = list(plan["branchProtection"]["desired"]["status_check_contexts"]) protection_changed = current_contexts != expected_contexts if not protection_changed: return self._envelope( context, { **self._public_governance_plan(plan), "applyScope": apply_scope, "changed": False, "verified": True, "temporaryPushAllowlistUsed": False, }, source=f"gitea:{repository_ref}", ) self._writer().update_branch_protection( plan["_repository"], str(plan["defaultBranch"]), {"status_check_contexts": expected_contexts}, ) verified_plan = self._governance_plan_data(context, repository_ref) verified_protection = verified_plan["branchProtection"]["current"] expected_effective_protection = _branch_protection_effective_projection( current_protection ) actual_effective_protection = _branch_protection_effective_projection( verified_plan["_currentProtection"] ) actual_contexts = ( set(verified_protection.get("status_check_contexts") or []) if verified_protection is not None else set() ) if ( verified_protection is None or verified_protection.get("enable_status_check") is not True or actual_contexts != set(expected_contexts) or actual_effective_protection != expected_effective_protection or verified_plan["workflow"]["currentDigest"] != plan["workflow"]["currentDigest"] ): raise GiteaError( "GITEA_GOVERNANCE_VERIFY_FAILED", "Gitea did not retain the requested ProjectBrain protection-only contract", ) return self._envelope( context, { **self._public_governance_plan(verified_plan), "applyScope": apply_scope, "changed": True, "verified": True, "temporaryPushAllowlistUsed": False, }, source=f"gitea:{repository_ref}", ) if plan["blockers"]: raise GiteaError( "GITEA_GOVERNANCE_BLOCKED", "repository governance has unresolved blockers", ) if plan["compliant"]: return self._envelope( context, { **self._public_governance_plan(plan), "applyScope": apply_scope, "changed": False, "verified": True, }, source=f"gitea:{repository_ref}", ) writer = self._writer() repository = plan["_repository"] default_branch = str(plan["defaultBranch"]) workflow_action = str(plan["workflow"]["action"]) workflow_changed = False temporary_push_allowlist_used = False try: if workflow_action in {"create", "update"}: temporary_push_allowlist_used = self._write_managed_validation_workflow( context, repository, default_branch, workflow_action, str(plan["_workflowSha"]), plan["_currentProtection"], ) workflow_changed = True desired = dict(plan["branchProtection"]["desired"]) protection_action = str(plan["branchProtection"]["action"]) if protection_action == "create": writer.create_branch_protection(repository, desired) elif protection_action == "update": if plan["workflow"]["profile"] == "projectbrain": edit_payload = {"status_check_contexts": desired["status_check_contexts"]} else: edit_payload = { key: value for key, value in desired.items() if key not in {"rule_name", "branch_name"} } writer.update_branch_protection(repository, default_branch, edit_payload) except GiteaError: if workflow_changed: try: current_file = writer.file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), default_branch ) _, _, current_sha = decode_file_payload(current_file, 200_000) if workflow_action == "create": writer.delete_file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), branch=default_branch, message="ci: roll back incomplete governance [skip ci]", sha=current_sha, ) elif plan["_workflowContent"] is not None: writer.update_file( repository, tuple(MANAGED_VALIDATION_PATH.split("/")), branch=default_branch, content=str(plan["_workflowContent"]), message="ci: roll back incomplete governance [skip ci]", sha=current_sha, ) except GiteaError: pass raise verified_plan = self._governance_plan_data(context, repository_ref) is_projectbrain_profile = plan["workflow"]["profile"] == "projectbrain" verified_contexts = set( (verified_plan["branchProtection"]["current"] or {}).get("status_check_contexts") or [] ) verified_expected_contexts = set( plan["branchProtection"]["desired"].get("status_check_contexts") or [] ) projectbrain_protection_changed = is_projectbrain_profile and ( verified_contexts != verified_expected_contexts or ( plan["_currentProtection"] is not None and _branch_protection_effective_projection(verified_plan["_currentProtection"]) != _branch_protection_effective_projection(plan["_currentProtection"]) ) ) projectbrain_workflow_changed = ( is_projectbrain_profile and verified_plan["workflow"]["currentDigest"] != plan["workflow"]["currentDigest"] ) if ( not verified_plan["compliant"] or projectbrain_protection_changed or projectbrain_workflow_changed ): raise GiteaError( "GITEA_GOVERNANCE_VERIFY_FAILED", "Gitea did not retain the requested governance contract", ) return self._envelope( context, { **self._public_governance_plan(verified_plan), "applyScope": apply_scope, "changed": True, "verified": True, "temporaryPushAllowlistUsed": temporary_push_allowlist_used, }, source=f"gitea:{repository_ref}", ) def _portfolio_governance( self, context: ConnectorContext, repository_refs: list[str] ) -> dict[str, Any]: if not 1 <= len(repository_refs) <= 25 or len(set(repository_refs)) != len(repository_refs): raise GiteaError( "GITEA_INPUT_INVALID", "repositoryRefs must contain 1-25 unique references" ) items: list[dict[str, Any]] = [] for repository_ref in repository_refs: try: items.append( self._public_governance_plan( self._governance_plan_data(context, repository_ref) ) ) except GiteaError as exc: items.append( {"repositoryRef": repository_ref, "compliant": False, "errorCode": exc.code} ) return { "items": items, "summary": { "total": len(items), "compliant": sum(bool(item.get("compliant")) for item in items), "attention": sum(not bool(item.get("compliant")) for item in items), }, } @governed_tool("gitea.portfolio.governance.preview") def portfolio_governance_preview( self, context: ConnectorContext, repository_refs: list[str] ) -> dict[str, Any]: return self._envelope( context, self._portfolio_governance(context, repository_refs), source="gitea:portfolio-governance", ) @governed_tool("gitea.portfolio.governance.verify") def portfolio_governance_verify( self, context: ConnectorContext, repository_refs: list[str] ) -> dict[str, Any]: return self.portfolio_governance_preview(context, repository_refs) @governed_tool("gitea.portfolio.governance.apply") def portfolio_governance_apply( self, context: ConnectorContext, plans: list[dict[str, str]] ) -> dict[str, Any]: if not 1 <= len(plans) <= _GOVERNANCE_BATCH_LIMIT: raise GiteaError("GITEA_INPUT_INVALID", "plans must contain 1-5 governance plans") repository_refs = [str(item.get("repositoryRef", "")) for item in plans] if len(set(repository_refs)) != len(repository_refs): raise GiteaError("GITEA_INPUT_INVALID", "governance repositoryRefs must be unique") items: list[dict[str, Any]] = [] for item in plans: repository_ref = str(item.get("repositoryRef", "")) try: apply_scope = str(item.get("applyScope", "all")) result = self.branch_protection_ensure( context, repository_ref, str(item.get("planHash", "")), apply_scope=apply_scope, # type: ignore[arg-type] )["data"] items.append( { "repositoryRef": repository_ref, "applied": True, "changed": bool(result.get("changed")), "verified": bool(result.get("verified")), "applyScope": str(result.get("applyScope", "all")), } ) except GiteaError as exc: items.append( {"repositoryRef": repository_ref, "applied": False, "errorCode": exc.code} ) return self._envelope( context, { "items": items, "summary": { "total": len(items), "applied": sum(bool(item.get("applied")) for item in items), "failed": sum(not bool(item.get("applied")) for item in items), }, }, source="gitea:portfolio-governance", ) @governed_tool("gitea.branch.list") def branch_list( self, context: ConnectorContext, repository_ref: str, *, page: PageRequest | None = None ) -> dict[str, Any]: def map_items(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: return [ { "name": str(item.get("name", ""))[:300], "commitSha": str((item.get("commit") or {}).get("id", ""))[:64], "protected": bool(item.get("protected", False)), } for item in items ] return self._collection( context, repository_ref, page, self.client.branches, map_items, "branches" ) @governed_tool("gitea.commit.list") def commit_list( self, context: ConnectorContext, repository_ref: str, *, page: PageRequest | None = None ) -> dict[str, Any]: def map_items(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: result = [] for item in items: commit_value = item.get("commit") commit = commit_value if isinstance(commit_value, dict) else {} author_value = commit.get("author") author = author_value if isinstance(author_value, dict) else {} result.append( { "sha": str(item.get("sha", ""))[:64], "summary": safe_snippet(str(commit.get("message", "")), max_bytes=500), "authorName": str(author.get("name", ""))[:200], "committedAt": str(author.get("date", ""))[:64], } ) return result return self._collection( context, repository_ref, page, self.client.commits, map_items, "commits" ) @staticmethod def _commit_projection(item: dict[str, Any]) -> dict[str, Any]: commit_value = item.get("commit") commit = commit_value if isinstance(commit_value, dict) else {} author_value = commit.get("author") author = author_value if isinstance(author_value, dict) else {} verification_value = item.get("verification") verification = verification_value if isinstance(verification_value, dict) else {} stats_value = item.get("stats") stats = stats_value if isinstance(stats_value, dict) else {} files_value = item.get("files") files = files_value if isinstance(files_value, list) else [] return { "sha": str(item.get("sha", ""))[:64], "summary": safe_snippet(str(commit.get("message", "")), max_bytes=2_000), "authorName": str(author.get("name", ""))[:200], "committedAt": str(author.get("date", ""))[:64], "verified": bool(verification.get("verified", False)), "verificationReason": str(verification.get("reason", ""))[:300], "stats": { "additions": max(int(stats.get("additions", 0) or 0), 0), "deletions": max(int(stats.get("deletions", 0) or 0), 0), "total": max(int(stats.get("total", 0) or 0), 0), }, "files": [ { "path": str(file.get("filename", ""))[:2_048], "status": str(file.get("status", ""))[:32], } for file in files[:100] if isinstance(file, dict) ], "filesTruncated": len(files) > 100, "sourceUri": str(item.get("html_url", ""))[:2_048], } @governed_tool("gitea.commit.get") def commit_get( self, context: ConnectorContext, repository_ref: str, ref: str ) -> dict[str, Any]: repository = self._repo(context, repository_ref) safe_ref = _validate_ref(ref) data = self._commit_projection(self.client.commit(repository, safe_ref)) data["repositoryRef"] = repository_ref return self._envelope( context, data, source=f"gitea:{repository_ref}", truncated=bool(data["filesTruncated"]), ) @governed_tool("gitea.ref.compare") def ref_compare( self, context: ConnectorContext, repository_ref: str, base: str, head: str ) -> dict[str, Any]: repository = self._repo(context, repository_ref) safe_base = _validate_ref(base) safe_head = _validate_ref(head) if safe_base == safe_head: raise GiteaError("GITEA_TARGET_INVALID", "comparison refs must differ") payload = self.client.compare(repository, safe_base, safe_head) raw_commits = payload.get("commits") raw_files = payload.get("files") commits = raw_commits if isinstance(raw_commits, list) else [] files = raw_files if isinstance(raw_files, list) else [] truncated = len(commits) > 100 or len(files) > 200 return self._envelope( context, { "repositoryRef": repository_ref, "base": safe_base, "head": safe_head, "totalCommits": max(int(payload.get("total_commits", len(commits)) or 0), 0), "commits": [ self._commit_projection(item) for item in commits[:100] if isinstance(item, dict) ], "files": [ { "path": str(item.get("filename", ""))[:2_048], "status": str(item.get("status", ""))[:32], } for item in files[:200] if isinstance(item, dict) ], "truncated": truncated, }, source=f"gitea:{repository_ref}", truncated=truncated, ) @governed_tool("gitea.release.list") def release_list( self, context: ConnectorContext, repository_ref: str, *, page: PageRequest | None = None ) -> dict[str, Any]: def map_items(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: return [ { "tag": str(item.get("tag_name", ""))[:300], "title": safe_snippet(str(item.get("name", "")), max_bytes=500), "draft": bool(item.get("draft", False)), "prerelease": bool(item.get("prerelease", False)), } for item in items ] return self._collection( context, repository_ref, page, self.client.releases, map_items, "releases" ) @governed_tool("gitea.issue.list") def issue_list( self, context: ConnectorContext, repository_ref: str, *, page: PageRequest | None = None ) -> dict[str, Any]: def map_items(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: return [ { "number": int(item.get("number", 0)), "title": safe_snippet(str(item.get("title", "")), max_bytes=500), "state": str(item.get("state", ""))[:32], "sourceUri": str(item.get("html_url", ""))[:2048], } for item in items ] return self._collection( context, repository_ref, page, self.client.issues, map_items, "issues" ) @governed_tool("gitea.issue.search") def issue_search( self, context: ConnectorContext, state: str = "open", exclude_repository_names: list[str] | None = None, *, page: PageRequest | None = None, ) -> dict[str, Any]: """Return a portfolio issue page without per-repository metadata calls.""" if state not in {"open", "closed", "all"}: raise GiteaError("GITEA_INPUT_INVALID", "issue state is invalid") excluded = { name.strip().casefold() for name in (exclude_repository_names or []) if name.strip() } if len(excluded) > 20 or any(len(name) > 120 for name in excluded): raise GiteaError("GITEA_INPUT_INVALID", "issue repository exclusion is invalid") self._refresh_discovered(context.project_id) repositories = self.registry.list(context.project_id) repository_by_name = {f"{item.owner}/{item.name}".casefold(): item for item in repositories} request = page or PageRequest(limit=100) cursor = decode_cursor( request.cursor, str(context.project_id), "portfolio-issues", state, ",".join(sorted(excluded)), str(request.limit), ) page_number = cursor.offset // request.limit + 1 raw_items = self.client.search_issues(state=state, page=page_number, limit=request.limit) items: list[dict[str, Any]] = [] for item in raw_items: raw_repository = item.get("repository") repository_data = raw_repository if isinstance(raw_repository, dict) else {} full_name = str(repository_data.get("full_name", "")) repository = repository_by_name.get(full_name.casefold()) if repository is None or repository.name.casefold() in excluded: continue raw_labels = item.get("labels") labels = raw_labels if isinstance(raw_labels, list) else [] items.append( { "repositoryRef": repository.repository_ref, "repositoryFullName": full_name[:242], "number": max(int(item.get("number", 0)), 0), "title": safe_snippet(str(item.get("title", "")), max_bytes=500), "state": str(item.get("state", ""))[:32], "updatedAt": str(item.get("updated_at", ""))[:64], "labels": [ safe_snippet(str(label.get("name", "")), max_bytes=100) for label in labels[:20] if isinstance(label, dict) ], "sourceUri": str(item.get("html_url", ""))[:2048], } ) has_more = len(raw_items) >= request.limit next_cursor = ( encode_cursor( cursor.offset + request.limit, str(context.project_id), "portfolio-issues", state, ",".join(sorted(excluded)), str(request.limit), ) if has_more else None ) return self._envelope( context, {"items": items, "nextCursor": next_cursor, "truncated": has_more}, source="gitea:portfolio-issues", truncated=has_more, next_cursor=next_cursor, ) @governed_tool("gitea.pullrequest.list") def pull_request_list( self, context: ConnectorContext, repository_ref: str, *, page: PageRequest | None = None ) -> dict[str, Any]: def map_items(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: return [self._pull_request_projection(item) for item in items] return self._collection( context, repository_ref, page, self.client.pull_requests, map_items, "pull-requests", ) @staticmethod def _pull_request_projection(item: dict[str, Any]) -> dict[str, Any]: raw_head = item.get("head") raw_base = item.get("base") head: dict[str, Any] = raw_head if isinstance(raw_head, dict) else {} base: dict[str, Any] = raw_base if isinstance(raw_base, dict) else {} return { "number": max(int(item.get("number", 0)), 0), "title": safe_snippet(str(item.get("title", "")), max_bytes=500), "state": str(item.get("state", ""))[:32], "draft": bool(item.get("draft", False)), "merged": bool(item.get("merged", False)), "mergeable": item.get("mergeable") if isinstance(item.get("mergeable"), bool) else None, "mergeCommitSha": str(item.get("merge_commit_sha", ""))[:64], "headRef": str(head.get("ref", ""))[:300], "headSha": str(head.get("sha", ""))[:64], "baseRef": str(base.get("ref", ""))[:300], "baseSha": str(base.get("sha", ""))[:64], "updatedAt": str(item.get("updated_at", ""))[:64], "sourceUri": str(item.get("html_url", ""))[:2048], } @governed_tool("gitea.pullrequest.get") def pull_request_get( self, context: ConnectorContext, repository_ref: str, number: int ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") repository = self._repo(context, repository_ref) pull = self._pull_request_projection(self.client.pull_request(repository, number)) files = self.client.pull_request_files(repository, number) pull["changedFiles"] = [ { "path": str(item.get("filename", ""))[:2048], "status": str(item.get("status", ""))[:32], "additions": max(int(item.get("additions", 0)), 0), "deletions": max(int(item.get("deletions", 0)), 0), } for item in files[:100] ] pull["filesTruncated"] = len(files) > 100 return self._envelope( context, pull, source=f"gitea:{repository_ref}", truncated=len(files) > 100, ) @governed_tool("gitea.pullrequest.feedback") def pull_request_feedback( self, context: ConnectorContext, repository_ref: str, number: int ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") repository = self._repo(context, repository_ref) reviews = self.client.pull_request_reviews(repository, number, 1, 100) output: list[dict[str, Any]] = [] truncated = len(reviews) > 100 for review in reviews[:100]: review_id = max(int(review.get("id", 0)), 0) comments = ( self.client.pull_request_review_comments(repository, number, review_id) if review_id else [] ) output.append( { "id": review_id, "state": str(review.get("state", ""))[:32], "body": safe_snippet(str(review.get("body", "")), max_bytes=4_000), "reviewer": str((review.get("user") or {}).get("login", ""))[:120], "submittedAt": str(review.get("submitted_at", ""))[:64], "comments": [ { "id": max(int(item.get("id", 0)), 0), "path": str(item.get("path", ""))[:2_048], "line": max(int(item.get("new_position", 0) or 0), 0), "body": safe_snippet(str(item.get("body", "")), max_bytes=4_000), } for item in comments[:100] ], "commentsTruncated": len(comments) > 100, } ) truncated = truncated or len(comments) > 100 return self._envelope( context, {"repositoryRef": repository_ref, "number": number, "reviews": output}, source=f"gitea:{repository_ref}", truncated=truncated, ) @staticmethod def _action_run_projection(item: dict[str, Any]) -> dict[str, Any]: return { "id": max(int(item.get("id", item.get("workflow_run_id", 0))), 0), "name": str(item.get("name", item.get("display_title", "")))[:300], "status": str(item.get("status", ""))[:32], "conclusion": str(item.get("conclusion", ""))[:32], "event": str(item.get("event", ""))[:64], "headBranch": str(item.get("head_branch", ""))[:300], "headSha": str(item.get("head_sha", ""))[:64], "workflowId": str(item.get("workflow_id", ""))[:2_048], "displayTitle": safe_snippet(str(item.get("display_title", "")), max_bytes=500), "createdAt": str(item.get("created_at", ""))[:64], "updatedAt": str(item.get("updated_at", ""))[:64], "sourceUri": str(item.get("html_url", item.get("url", "")))[:2_048], } @staticmethod def _workflow_identity_values(workflow: dict[str, Any]) -> set[str]: values: set[str] = set() for key in ("id", "path"): value = str(workflow.get(key, "")).strip().casefold() if value: values.add(value) values.add(value.rsplit("/", 1)[-1]) return values @classmethod def _runs_for_workflow( cls, runs: list[dict[str, Any]], workflow: dict[str, Any] | None, *, allow_legacy_without_workflow_id: bool = True, ) -> list[dict[str, Any]]: if workflow is None: return [] identities = cls._workflow_identity_values(workflow) if not identities: return [] if not any(str(item.get("workflow_id", "")).strip() for item in runs): return runs if allow_legacy_without_workflow_id else [] return [ item for item in runs if str(item.get("workflow_id", "")).strip().casefold() in identities or str(item.get("workflow_id", "")).strip().casefold().rsplit("/", 1)[-1] in identities ] def _find_reusable_validation_run( self, repository: RegisteredRepository, branch: str, head_sha: str, profile: str, workflow: dict[str, Any], ) -> dict[str, Any] | None: runs = self.client.action_runs(repository, branch=branch, page=1, limit=100) identities = self._workflow_identity_values(workflow) for raw in runs: workflow_id = str(raw.get("workflow_id", "")).strip().casefold() if not workflow_id: continue if workflow_id not in identities and workflow_id.rsplit("/", 1)[-1] not in identities: continue candidate = self._action_run_projection(raw) if ( str(candidate.get("headSha", "")).casefold() != head_sha.casefold() or str(candidate.get("event", "")).casefold() != "workflow_dispatch" ): continue status = str(candidate.get("status", "")).casefold() conclusion = str(candidate.get("conclusion", "")).casefold() if status == "completed" and conclusion != "success": continue run_id = max(int(candidate.get("id", 0) or 0), 0) if not run_id: continue expected_title = f"Managed validation / {profile}" if str(candidate.get("displayTitle", "")).casefold() == expected_title.casefold(): return candidate return None @governed_tool("gitea.validation.get") def validation_get( self, context: ConnectorContext, repository_ref: str, run_id: int, include_logs: bool = False, ) -> dict[str, Any]: if not 1 <= run_id <= 9_223_372_036_854_775_807: raise GiteaError("GITEA_TARGET_INVALID", "workflow run id is invalid") repository = self._repo(context, repository_ref) run = self._action_run_projection(self.client.action_run(repository, run_id)) jobs = self.client.action_run_jobs(repository, run_id) projected_jobs: list[dict[str, Any]] = [] any_truncated = len(jobs) > 100 for job in jobs[:100]: job_id = max(int(job.get("id", 0)), 0) logs = "" logs_truncated = False if include_logs and job_id: logs, logs_truncated = self.client.action_job_logs(repository, job_id) filtered_logs = safe_content(logs) logs = filtered_logs or "[log excluded: secret-like content detected]" projected_jobs.append( { "id": job_id, "name": str(job.get("name", ""))[:300], "status": str(job.get("status", ""))[:32], "conclusion": str(job.get("conclusion", ""))[:32], "startedAt": str(job.get("started_at", ""))[:64], "completedAt": str(job.get("completed_at", ""))[:64], "logs": logs, "logsTruncated": logs_truncated, } ) any_truncated = any_truncated or logs_truncated run["jobs"] = projected_jobs run.update(_validation_polling(run, str(run.get("headSha", "")))) return self._envelope( context, {"repositoryRef": repository_ref, "run": run}, source=f"gitea:{repository_ref}", truncated=any_truncated, ) @governed_tool("gitea.validation.failure.get") def validation_failure_get( self, context: ConnectorContext, repository_ref: str, run_id: int, ) -> dict[str, Any]: """Return only bounded, failure-relevant validation evidence.""" if not 1 <= run_id <= 9_223_372_036_854_775_807: raise GiteaError("GITEA_TARGET_INVALID", "workflow run id is invalid") repository = self._repo(context, repository_ref) run = self._action_run_projection(self.client.action_run(repository, run_id)) jobs = self.client.action_run_jobs(repository, run_id) failures: list[dict[str, Any]] = [] truncated = len(jobs) > 100 remaining_lines = _FAILURE_MAX_LINES remaining_bytes = _FAILURE_MAX_BYTES for job in jobs[:100]: conclusion = str(job.get("conclusion", "")).casefold() status = str(job.get("status", "")).casefold() if conclusion not in _FAILED_CONCLUSIONS and status not in _FAILED_CONCLUSIONS: continue if len(failures) >= _FAILURE_MAX_JOBS or remaining_lines <= 0 or remaining_bytes <= 0: truncated = True break job_id = max(int(job.get("id", 0) or 0), 0) logs, logs_truncated = self.client.action_job_logs( repository, job_id, max_bytes=200_000 ) filtered_logs = safe_content(logs) if filtered_logs is None: excerpt, exit_code, command, error_code = ( ["[log excluded: secret-like content detected]"], None, None, "VALIDATION_LOG_EXCLUDED", ) else: excerpt, exit_code, command, error_code = self._failure_excerpt(filtered_logs) bounded_excerpt: list[str] = [] for line in excerpt[:remaining_lines]: encoded_size = len(line.encode("utf-8")) if encoded_size > remaining_bytes: truncated = True break bounded_excerpt.append(line) remaining_bytes -= encoded_size remaining_lines -= len(bounded_excerpt) if len(bounded_excerpt) < len(excerpt): truncated = True excerpt = bounded_excerpt raw_steps = job.get("steps") steps = raw_steps if isinstance(raw_steps, list) else [] failed_step = next( ( str(item.get("name", ""))[:300] for item in steps if isinstance(item, dict) and str(item.get("conclusion", "")).casefold() in _FAILED_CONCLUSIONS ), None, ) failures.append( { "jobId": job_id, "job": str(job.get("name", ""))[:300], "step": failed_step, "command": command, "exitCode": exit_code, "errorCode": error_code, "relevantLogs": excerpt, "logsTruncated": logs_truncated or len(excerpt) >= 100, "artifactRefs": [run["sourceUri"]] if run.get("sourceUri") else [], } ) truncated = truncated or logs_truncated or remaining_lines <= 0 or remaining_bytes <= 0 return self._envelope( context, { "repositoryRef": repository_ref, "runId": run_id, "headSha": run.get("headSha"), "conclusion": run.get("conclusion"), "failures": failures, "failureCount": len(failures), "fullLogsIncluded": False, }, source=f"gitea:{repository_ref}", truncated=truncated, ) @governed_tool("gitea.validation.list") def validation_list( self, context: ConnectorContext, repository_ref: str, branch: str, *, page: PageRequest | None = None, ) -> dict[str, Any]: repository = self._repo(context, repository_ref) safe_branch = _validate_ref(branch) request = page or PageRequest(limit=25) cursor = decode_cursor( request.cursor, str(context.project_id), repository_ref, f"validations:{safe_branch}", str(request.limit), ) page_number = cursor.offset // request.limit + 1 raw_runs = self.client.action_runs( repository, branch=safe_branch, page=page_number, limit=request.limit ) workflow = _select_validation_workflow(self.client.action_workflows(repository)) runs = self._runs_for_workflow(raw_runs, workflow) next_cursor = None if len(raw_runs) == request.limit and workflow is not None: next_cursor = encode_cursor( cursor.offset + len(runs), str(context.project_id), repository_ref, f"validations:{safe_branch}", str(request.limit), ) return self._envelope( context, { "repositoryRef": repository_ref, "branch": safe_branch, "items": [self._action_run_projection(item) for item in runs], "nextCursor": next_cursor, "truncated": next_cursor is not None, }, source=f"gitea:{repository_ref}", truncated=next_cursor is not None, next_cursor=next_cursor, ) @governed_tool("gitea.change.status") def change_status( self, context: ConnectorContext, repository_ref: str, branch: str ) -> dict[str, Any]: safe_branch = _validate_ref(branch) repository = self._repo(context, repository_ref) branches = self.client.branches(repository, 1, 100) branch_payload = next( (item for item in branches if str(item.get("name", "")) == safe_branch), None ) if branch_payload is None: raise GiteaError("GITEA_NOT_FOUND", "change branch was not found") pulls = self.client.pull_requests(repository, 1, 100) pull = next( ( self._pull_request_projection(item) for item in pulls if str((item.get("head") or {}).get("ref", "")) == safe_branch ), None, ) workflow = _select_validation_workflow(self.client.action_workflows(repository)) runs = self._runs_for_workflow( self.client.action_runs(repository, branch=safe_branch, page=1, limit=100), workflow ) projected_runs = [self._action_run_projection(item) for item in runs] latest_run = projected_runs[0] if projected_runs else None commit_sha = str((branch_payload.get("commit") or {}).get("id", ""))[:64] merge_eligibility: dict[str, Any] = { "eligible": False, "headSha": commit_sha, "reason": "pull_request_missing" if pull is None else "not_evaluated", } if pull is not None: try: _, base_sha, run_id, _ = self._require_merge_eligible( context, repository_ref, int(pull["number"]), commit_sha ) except GiteaError as exc: merge_eligibility.update( {"reason": exc.code, "detail": str(exc), "baseSha": pull.get("baseSha")} ) else: merge_eligibility.update( { "eligible": True, "reason": "all_machine_verifiable_gates_passed", "baseSha": base_sha, "validationContext": MANAGED_VALIDATION_CONTEXT, "validationRunId": run_id, } ) ready_to_merge = bool(merge_eligibility["eligible"]) next_actions = [] if pull is None: next_actions.append("create-pull-request") if latest_run is None or latest_run.get("headSha") != commit_sha: next_actions.append("run-validation") elif latest_run.get("status") != "completed": next_actions.append("wait-for-validation") elif latest_run.get("conclusion") != "success": next_actions.append("fix-validation") if pull is not None and not ready_to_merge: if merge_eligibility["reason"] in { "GITEA_PR_CHECK_MISSING", "GITEA_PR_CHECK_PENDING", "GITEA_PR_CHECK_FAILED", "GITEA_PR_VALIDATION_REQUIRED", }: next_actions.append("ensure-pr-validation") elif merge_eligibility["reason"] in {"GITEA_BRANCH_OUTDATED", "GITEA_BASE_CHANGED"}: next_actions.append("sync-pull-request") if ready_to_merge: next_actions.append("review-and-merge") snapshot_at = datetime.now(UTC).isoformat() snapshot_material = { "repositoryRef": repository_ref, "branch": safe_branch, "commitSha": commit_sha, "pullRequest": pull, "validationRuns": projected_runs, } return self._envelope( context, { "repositoryRef": repository_ref, "branch": safe_branch, "commitSha": commit_sha, "pullRequest": pull, "validationRuns": projected_runs, **_validation_polling(latest_run, commit_sha), "mergeEligibility": merge_eligibility, "requiredPrValidationState": ( "success" if ready_to_merge else str(merge_eligibility["reason"]) ), "validationState": ( "pr_check_missing" if merge_eligibility["reason"] in {"GITEA_PR_CHECK_MISSING", "GITEA_PR_VALIDATION_REQUIRED"} else "pr_check_pending" if merge_eligibility["reason"] == "GITEA_PR_CHECK_PENDING" else "pr_check_failed" if merge_eligibility["reason"] == "GITEA_PR_CHECK_FAILED" else _validation_polling(latest_run, commit_sha)["validationState"] ), "nextAction": ( "ensure-pr-validation" if "ensure-pr-validation" in next_actions else _validation_polling(latest_run, commit_sha)["nextAction"] ), "readyToMerge": ready_to_merge, "nextActions": next_actions, "resumeRef": f"{repository_ref}:{safe_branch}", "snapshotId": self._snapshot_id(snapshot_material), "snapshotAt": snapshot_at, "giteaRevision": commit_sha, }, source=f"gitea:{repository_ref}", ) @governed_tool("gitea.change.review") def change_review( self, context: ConnectorContext, repository_ref: str, branch: str ) -> dict[str, Any]: repository = self._repo(context, repository_ref) status: dict[str, Any] = {} pull: dict[str, Any] | None = None comparison: dict[str, Any] | None = None verified: dict[str, Any] = {} stable = False observed_actual = "" for _ in range(2): status = self.change_status(context, repository_ref, branch)["data"] candidate = status.get("pullRequest") if not isinstance(candidate, dict): pull = None comparison = None else: pull = self._pull_request_projection( self.client.pull_request(repository, max(int(candidate.get("number", 0)), 0)) ) if pull.get("headSha") != status.get("commitSha"): observed_actual = str(pull.get("headSha", "")) continue status["pullRequest"] = pull comparison = None if pull.get("baseSha") and pull.get("headSha"): payload = self.client.compare( repository, str(pull["baseSha"]), str(pull["headSha"]) ) reverse_payload = self.client.compare( repository, str(pull["headSha"]), str(pull["baseSha"]) ) comparison = self._compact_comparison( payload, base=str(pull.get("baseRef", "")), head=str(pull.get("headRef", "")), reverse_payload=reverse_payload, ) verified = self.change_status(context, repository_ref, branch)["data"] verified_candidate = verified.get("pullRequest") stable = ( verified.get("commitSha") == status.get("commitSha") and verified.get("validationRuns") == status.get("validationRuns") and isinstance(verified_candidate, dict) == isinstance(candidate, dict) ) if stable and pull is not None and isinstance(verified_candidate, dict): verified_pull = self._pull_request_projection( self.client.pull_request( repository, max(int(verified_candidate.get("number", 0)), 0) ) ) stable = ( verified_pull.get("headSha") == pull.get("headSha") and verified_pull.get("baseSha") == pull.get("baseSha") and verified_pull.get("state") == pull.get("state") ) if stable: pull = verified_pull verified["pullRequest"] = verified_pull if stable: status = verified break if not stable: raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "change state moved while the review snapshot was being created", retryable=True, safe_to_retry=True, expected_state=str(status.get("commitSha", status.get("snapshotId", ""))), actual_state=( observed_actual or str(verified.get("commitSha", verified.get("snapshotId", ""))) ), recovery_hint="Retry change.review to obtain one consistent snapshot.", ) snapshot_material = {**status, "comparison": comparison} snapshot_at = datetime.now(UTC).isoformat() return self._envelope( context, { **status, "comparison": comparison, "snapshotId": self._snapshot_id(snapshot_material), "snapshotAt": snapshot_at, "giteaRevision": status.get("commitSha"), "snapshotConsistent": True, "snapshotConsistency": "optimistic-double-read", }, source=f"gitea:{repository_ref}", truncated=bool(comparison and comparison.get("truncated")), ) @governed_tool("gitea.change.reconcile") def change_reconcile( self, context: ConnectorContext, repository_ref: str, branch: str ) -> dict[str, Any]: """Compute one non-mutating delivery recommendation from a consistent snapshot.""" review = self.change_review(context, repository_ref, branch)["data"] pull = review.get("pullRequest") if isinstance(review.get("pullRequest"), dict) else None comparison = review.get("comparison") if isinstance(review.get("comparison"), dict) else {} recommendation = "create-pull-request" if pull is not None: if pull.get("mergeable") is False or int(comparison.get("behindBy", 0) or 0) > 0: recommendation = "sync-or-resolve-conflicts" elif review.get("validationState") == "failed": recommendation = "inspect-validation-failure" elif review.get("validationState") in {"not-started", "stale"}: recommendation = "run-validation" elif not review.get("terminal", True): recommendation = "wait-for-validation" elif review.get("readyToMerge"): recommendation = "review-and-merge" else: recommendation = "review-pull-request" return self._envelope( context, { "repositoryRef": repository_ref, "branch": branch, "snapshotId": review["snapshotId"], "snapshotAt": review["snapshotAt"], "giteaRevision": review["giteaRevision"], "pullRequest": pull, "branchBehindBy": int(comparison.get("behindBy", 0) or 0), "validationState": review.get("validationState"), "readyToMerge": bool(review.get("readyToMerge", False)), "recommendedNextAction": recommendation, "validationRequired": recommendation in { "run-validation", "sync-or-resolve-conflicts", }, "mutationPerformed": False, }, source=f"gitea:{repository_ref}", ) def _capability_data(self, context: ConnectorContext) -> dict[str, Any]: publications = list(self.tools()) if self.write_client is not None: publications.extend(self.write_tools()) descriptors = sorted( [ { "name": item.canonical_name, "version": item.version, "riskClass": item.risk_class, "inputSchema": item.input_schema, } for item in publications ], key=lambda item: str(item["name"]), ) encoded = json.dumps(descriptors, sort_keys=True, separators=(",", ":")).encode() risk_counts = { risk: sum(item.risk_class == risk for item in publications) for risk in ("R1", "R2", "R3") } published_names = {str(item["name"]) for item in descriptors} handler_names = set(self._tool_handlers()) runtime = runtime_snapshot() return { "server": "itworx-gitea", "version": self.version, "buildSha": os.environ.get("GITEA_CONNECTOR_BUILD_SHA", "unknown")[:64], "capabilityFingerprint": hashlib.sha256(encoded).hexdigest(), "toolCount": len(publications), "registeredHandlerCount": len(handler_names), "missingHandlers": sorted(published_names - handler_names), "extraHandlers": sorted(handler_names - published_names), "riskCounts": risk_counts, "tools": [item["name"] for item in descriptors], "repositoryCount": len(self.registry.list(context.project_id)), "writeMode": "pr-first" if self.write_client is not None else "disabled", "workspaceProfiles": list(_WORKSPACE_PROFILES), "changeBranchPrefixes": [], "workBranchPolicy": "any-valid-non-default-branch", "arbitraryExecution": False, "durableStateSource": "gitea-branches-pullrequests-actions", "selfTestAvailable": True, "selfTestOperation": "gitea.selftest.run", "selfTestModes": ["read", "guarded-write"], "runtime": runtime, "subsystems": { "runtime": "READY" if published_names == handler_names else "DEGRADED", "catalog": "READY" if published_names == handler_names else "DEGRADED", "dispatcher": "READY", "worker": "READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED", "upstream_gitea": "UNVERIFIED", "search": "READY", "write": "READY" if self.write_client is not None else "DISABLED", "actions": "READY" if self.write_client is not None else "READ_ONLY", "projectbrain": "EXTERNAL", "database": "NOT_APPLICABLE", "queue": "READY", "session": "RESUMABLE", }, } @governed_tool("gitea.capabilities.get") def capabilities_get(self, context: ConnectorContext) -> dict[str, Any]: """Return a stable fingerprint for client-side tool catalog refresh checks.""" return self._envelope(context, self._capability_data(context), source="gitea:capabilities") @governed_tool("gitea.connection.check") def connection_check(self, context: ConnectorContext) -> dict[str, Any]: """Verify scoped upstream access and discovery without returning credentials.""" self._refresh_discovered(context.project_id) repositories = self.registry.list(context.project_id) if not repositories: raise GiteaError("GITEA_REPOSITORY_NOT_REGISTERED", "no repositories are registered") probe = self.client.repository(repositories[0]) capability = self._capability_data(context) return self._envelope( context, { "connected": True, "upstream": "gitea", "repositoryCount": len(repositories), "probeRepositoryRef": repositories[0].repository_ref, "probePrivate": bool(probe.get("private", False)), "capabilityFingerprint": capability["capabilityFingerprint"], "toolCount": capability["toolCount"], "credentialMode": "mounted-secret", }, source="gitea:connection", ) @governed_tool("gitea.selftest.run") def selftest_run(self, context: ConnectorContext, mode: str = "read") -> dict[str, Any]: """Run bounded, non-mutating runtime and upstream registration probes.""" if mode not in {"read", "guarded-write"}: raise GiteaError("GITEA_INPUT_INVALID", "self-test mode is not supported") capability = self._capability_data(context) connection = self.connection_check(context)["data"] checks = [ { "name": "handler-parity", "status": "PASS" if not capability["missingHandlers"] and not capability["extraHandlers"] else "FAIL", "published": capability["toolCount"], "registered": capability["registeredHandlerCount"], }, { "name": "gitea-connection", "status": "PASS" if connection["connected"] else "FAIL", "repositoryCount": connection["repositoryCount"], }, { "name": "write-runtime", "status": "PASS" if self.write_client is not None else "DISABLED", "mode": capability["writeMode"], }, { "name": "managed-actions", "status": "PASS" if self.write_client is not None else "READ_ONLY", "profiles": list(_WORKSPACE_PROFILES), }, ] failed = [item["name"] for item in checks if item["status"] == "FAIL"] return self._envelope( context, { "status": "PASS" if not failed else "DEGRADED", "checks": checks, "failedChecks": failed, "probeOnly": True, "mutationsAttempted": 0, "mode": mode, "availableModes": capability["selfTestModes"], "canonicalOperation": capability["selfTestOperation"], "sessionRecovery": capability["runtime"], "capabilityFingerprint": capability["capabilityFingerprint"], "buildSha": capability["buildSha"], }, source="gitea:selftest", ) @governed_tool("gitea.workspace.status") def workspace_status( self, context: ConnectorContext, repository_ref: str, branch: str ) -> dict[str, Any]: """Reconstruct a durable repository workspace from Gitea source-of-truth state.""" safe_branch = _validate_ref(branch) status = self.change_status(context, repository_ref, safe_branch)["data"] workspace_id = hashlib.sha256( f"{context.project_id}:{repository_ref}:{safe_branch}".encode() ).hexdigest()[:24] durable_status = {key: value for key, value in status.items() if key != "snapshotAt"} state_version = hashlib.sha256( json.dumps(durable_status, sort_keys=True, separators=(",", ":")).encode() ).hexdigest()[:24] return self._envelope( context, { **status, "workspaceId": workspace_id, "stateVersion": state_version, "execution": { "mode": "managed-actions", "profiles": list(_WORKSPACE_PROFILES), "timeoutMinutes": 30, "repositoryPermission": "read", "arbitraryCommands": False, }, }, source=f"gitea:{repository_ref}", ) @governed_tool("gitea.portfolio.health") def portfolio_health( self, context: ConnectorContext, repository_refs: list[str] ) -> dict[str, Any]: """Run bounded health checks across an explicit repository set.""" if not 1 <= len(repository_refs) <= 25 or len(set(repository_refs)) != len(repository_refs): raise GiteaError( "GITEA_INPUT_INVALID", "repositoryRefs must contain 1-25 unique references" ) items: list[dict[str, Any]] = [] for repository_ref in repository_refs: try: health = self.repository_health(context, repository_ref)["data"] items.append( { "repositoryRef": repository_ref, "healthy": bool(health.get("healthy", False)), "defaultBranch": health.get("defaultBranch"), "defaultBranchProtected": health.get("defaultBranchProtected"), "validationWorkflowAvailable": health.get("validationWorkflowAvailable"), "openPullRequestCount": health.get("openPullRequestCount", 0), "safeBranchCount": health.get("safeBranchCount", 0), "snapshotId": health.get("snapshotId"), "sourceRevision": health.get("sourceRevision"), "findings": health.get("findings", [])[:20], } ) except GiteaError as exc: items.append( { "repositoryRef": repository_ref, "healthy": False, "errorCode": exc.code, } ) snapshot_at = datetime.now(UTC).isoformat() portfolio_snapshot_id = self._snapshot_id( { "items": [ { "repositoryRef": item.get("repositoryRef"), "snapshotId": item.get("snapshotId"), "sourceRevision": item.get("sourceRevision"), "errorCode": item.get("errorCode"), } for item in items ], } ) return self._envelope( context, { "items": items, "snapshotId": portfolio_snapshot_id, "snapshotAt": snapshot_at, "cacheAgeMs": 0, "summary": { "total": len(items), "healthy": sum(bool(item.get("healthy")) for item in items), "attention": sum(not bool(item.get("healthy")) for item in items), }, }, source="gitea:portfolio-health", ) def _orchestration_plan_data( self, context: ConnectorContext, repository_refs: list[str], branch_suffix: str, ) -> dict[str, Any]: if not 1 <= len(repository_refs) <= 10 or len(set(repository_refs)) != len(repository_refs): raise GiteaError( "GITEA_INPUT_INVALID", "repositoryRefs must contain 1-10 unique references" ) if not _ORCHESTRATION_SUFFIX.fullmatch(branch_suffix): raise GiteaError("GITEA_INPUT_INVALID", "branchSuffix is outside the safe policy") branch = _validate_ref(f"codex/{branch_suffix}", change_branch=True) items: list[dict[str, Any]] = [] for repository_ref in repository_refs: repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) default_branch = str(metadata.get("default_branch", "")) if not default_branch: raise GiteaError( "GITEA_TARGET_INVALID", "repository does not have a default branch" ) base = self.client.branch(repository, default_branch) items.append( { "repositoryRef": repository_ref, "baseRef": default_branch, "baseSha": str((base.get("commit") or {}).get("id", ""))[:64], "branch": branch, } ) orchestration_id = hashlib.sha256( json.dumps(items, sort_keys=True, separators=(",", ":")).encode() ).hexdigest()[:24] return { "orchestrationId": orchestration_id, "branchSuffix": branch_suffix, "items": items, "repositoryCount": len(items), "nextAction": "confirm-and-start", } @governed_tool("gitea.orchestration.plan") def orchestration_plan( self, context: ConnectorContext, repository_refs: list[str], branch_suffix: str, ) -> dict[str, Any]: """Preview exact bases and safe branches for one bounded multi-repository task.""" return self._envelope( context, self._orchestration_plan_data(context, repository_refs, branch_suffix), source="gitea:orchestration", ) @governed_tool("gitea.workspace.run") def workspace_run( self, context: ConnectorContext, repository_ref: str, branch: str, profile: str, ) -> dict[str, Any]: """Dispatch an isolated fixed-profile workspace job with durable Gitea state.""" if profile not in _WORKSPACE_PROFILES: raise GiteaError("GITEA_INPUT_INVALID", "workspace profile is not allowlisted") result = self.validation_run(context, repository_ref, branch, profile) workspace = self.workspace_status(context, repository_ref, branch)["data"] result["data"].update( { "workspaceId": workspace["workspaceId"], "executionMode": "managed-actions", "timeoutMinutes": 30, "arbitraryCommands": False, } ) return result @governed_tool("gitea.orchestration.start") def orchestration_start( self, context: ConnectorContext, repository_refs: list[str], branch_suffix: str, expected_orchestration_id: str, ) -> dict[str, Any]: """Create or resume one safe branch in every preflighted repository.""" if not re.fullmatch(r"^[0-9a-f]{24}$", expected_orchestration_id): raise GiteaError("GITEA_INPUT_INVALID", "expectedOrchestrationId is invalid") plan = self._orchestration_plan_data(context, repository_refs, branch_suffix) if plan["orchestrationId"] != expected_orchestration_id: raise GiteaError( "GITEA_CONFLICT", "orchestration plan changed; refresh before starting" ) results: list[dict[str, Any]] = [] for item in plan["items"]: repository = self._repo(context, str(item["repositoryRef"])) try: existing = self.client.branch(repository, str(item["branch"])) except GiteaError as exc: if exc.code != "GITEA_NOT_FOUND": raise created = self.branch_create( context, str(item["repositoryRef"]), str(item["branch"]), str(item["baseRef"]), )["data"] results.append({**item, "state": "created", "headSha": created["commitSha"]}) else: results.append( { **item, "state": "resumed", "headSha": str((existing.get("commit") or {}).get("id", ""))[:64], } ) return self._envelope( context, { **plan, "items": results, "started": True, "nextAction": "apply-reviewed-changesets", }, source="gitea:orchestration", ) @governed_tool("gitea.commitstatus.list") def commit_status_list( self, context: ConnectorContext, repository_ref: str, ref: str, *, page: PageRequest | None = None, ) -> dict[str, Any]: if not 1 <= len(ref) <= 300 or any(char in ref for char in "\\?#\x00\r\n"): raise GiteaError("GITEA_TARGET_INVALID", "commit status ref is invalid") def loader(repository: RegisteredRepository, page_number: int, limit: int) -> Any: return self.client.commit_statuses(repository, ref, page_number, limit) def mapper(items: list[dict[str, Any]], repository: Any) -> list[dict[str, Any]]: return [ { "state": str(item.get("status", ""))[:32], "context": str(item.get("context", ""))[:200], "description": safe_snippet(str(item.get("description", "")), max_bytes=500), "targetUrl": str(item.get("target_url", ""))[:2048], "createdAt": str(item.get("created_at", ""))[:64], } for item in items ] return self._collection( context, repository_ref, page, loader, mapper, f"commit-statuses:{ref}" ) @governed_tool("gitea.file.read") def file_read( self, context: ConnectorContext, file_uri: str, ref: str | None = None ) -> dict[str, Any]: parsed = urlparse(file_uri) if ( parsed.scheme != "itworx-git" or parsed.query or parsed.fragment or parsed.username or parsed.password ): raise GiteaError("GITEA_FILE_URI_INVALID", "file URI is not a registered Gitea URI") if parsed.netloc != context.project_slug: raise GiteaError("GITEA_FILE_URI_INVALID", "file URI is outside the caller project") segments = [unquote(part) for part in parsed.path.split("/") if part] if len(segments) < 2 or any( "/" in part or "\\" in part or part in {".", ".."} for part in segments ): raise GiteaError("GITEA_PATH_INVALID", "file URI path is invalid") repository = self._repo(context, segments[0]) safe_ref = _validate_ref(ref) if ref else None payload = self.client.file(repository, tuple(segments[1:]), safe_ref) content, truncated, commit_sha = decode_file_payload(payload, 200_000) data = { "fileUri": file_uri, "mimeType": "text/plain", "content": content, "commitSha": commit_sha, "truncated": truncated, "generatedAt": datetime.now(UTC).isoformat(), } return self._envelope( context, data, source=f"gitea:{repository.repository_ref}", truncated=truncated ) @governed_tool("gitea.tree.list") def tree_list( self, context: ConnectorContext, repository_ref: str, path: str = "", ref: str | None = None, ) -> dict[str, Any]: repository = self._repo(context, repository_ref) safe_path = _validate_file_path(path) if path else () safe_ref = _validate_ref(ref) if ref else None payload = self.client.contents(repository, safe_path, safe_ref) entries = payload if isinstance(payload, list) else [payload] if not all(isinstance(item, dict) for item in entries): raise GiteaError("GITEA_INVALID_RESPONSE", "Gitea returned an invalid directory") items = [ { "name": str(item.get("name", ""))[:512], "path": str(item.get("path", ""))[:2048], "type": str(item.get("type", ""))[:32], "sha": str(item.get("sha", ""))[:64], "size": max(int(item.get("size", 0)), 0), "fileUri": ( f"itworx-git://{context.project_slug}/{repository_ref}/" f"{str(item.get('path', ''))}" ), } for item in entries[:1000] ] return self._envelope( context, {"items": items, "truncated": len(entries) > 1000}, source=f"gitea:{repository_ref}", truncated=len(entries) > 1000, ) @governed_tool("gitea.code.search") def code_search( self, context: ConnectorContext, repository_ref: str, query: str, ref: str | None = None, path_prefix: str = "", *, page: PageRequest | None = None, ) -> dict[str, Any]: if not 1 <= len(query) <= 500: raise GiteaError( "GITEA_QUERY_INVALID", "code search query is outside the connector budget" ) repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) selected_ref = _validate_ref(ref) if ref else str(metadata.get("default_branch", "")) safe_prefix = "/".join(_validate_file_path(path_prefix)) + "/" if path_prefix else "" request = page or PageRequest(limit=25) cursor = decode_cursor( request.cursor, str(context.project_id), repository_ref, selected_ref, query, safe_prefix, str(request.limit), ) tree_page_size = 1_000 tree_page = cursor.offset // tree_page_size + 1 page_offset = cursor.offset % tree_page_size tree_payload, cache_hit = self._git_tree_cached( repository, selected_ref, page=tree_page, per_page=tree_page_size ) raw_tree = tree_payload.get("tree") tree = raw_tree if isinstance(raw_tree, list) else [] mapped: list[dict[str, Any]] = [] scanned = 0 consumed = 0 needle = query.casefold() deadline = time.monotonic() + _CODE_SEARCH_BUDGET_SECONDS for candidate in tree[page_offset:]: if ( scanned >= _CODE_SEARCH_MAX_FILES or len(mapped) >= request.limit or time.monotonic() >= deadline ): break consumed += 1 if ( not isinstance(candidate, dict) or candidate.get("type") != "blob" or not str(candidate.get("path", "")).startswith(safe_prefix) or not 0 <= int(candidate.get("size", 0) or 0) <= 200_000 ): continue scanned += 1 path = str(candidate.get("path", "")) sha = str(candidate.get("sha", "")) try: payload = self.client.git_blob( repository, sha, timeout_seconds=_CODE_SEARCH_BLOB_TIMEOUT_SECONDS, max_attempts=1, ) except GiteaError as exc: # A single slow or transient blob must not make the whole MCP # request exceed the client deadline. Return the bounded partial # page and let its cursor continue from the next candidate. if exc.code == "GITEA_UNAVAILABLE": continue raise encoded = payload.get("content") if not isinstance(encoded, str): continue try: raw = base64.b64decode(encoded.encode("ascii"), validate=False) content = raw.decode("utf-8") except (ValueError, UnicodeDecodeError, UnicodeEncodeError): continue matches = [] for line_number, line in enumerate(content.splitlines(), 1): if needle in line.casefold() and not is_secret_like(line): matches.append( {"line": line_number, "snippet": safe_snippet(line, max_bytes=500)} ) if len(matches) == 10: break if matches: mapped.append( { "fileUri": f"itworx-git://{context.project_slug}/{repository_ref}/{path}", "repositoryRef": repository_ref, "path": path[:2048], "sha": sha[:64], "matches": matches, } ) absolute_offset = (tree_page - 1) * tree_page_size + page_offset + consumed more_results = page_offset + consumed < len(tree) or bool( tree_payload.get("truncated", False) ) next_cursor = ( encode_cursor( absolute_offset, str(context.project_id), repository_ref, selected_ref, query, safe_prefix, str(request.limit), ) if more_results else None ) search_truncated = next_cursor is not None data = { "items": mapped, "nextCursor": next_cursor, "truncated": search_truncated, "scannedFiles": scanned, } result = self._envelope( context, data, source=f"gitea:{repository_ref}:code-search", truncated=search_truncated, next_cursor=next_cursor, ) result["meta"]["cacheHit"] = cache_hit result["meta"]["cacheTtlSeconds"] = 30 return result @staticmethod def _research_id(context: ConnectorContext, repository_ref: str, path: str, ref: str) -> str: payload = json.dumps( { "project": context.project_slug, "repositoryRef": repository_ref, "path": path, "ref": ref, }, sort_keys=True, separators=(",", ":"), ).encode() return "gitea:" + base64.urlsafe_b64encode(payload).decode().rstrip("=") @staticmethod def _decode_research_id(context: ConnectorContext, identifier: str) -> dict[str, str]: if not identifier.startswith("gitea:") or len(identifier) > 4096: raise GiteaError("GITEA_DOCUMENT_INVALID", "research document ID is invalid") encoded = identifier.removeprefix("gitea:") try: raw = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)) value = json.loads(raw) except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise GiteaError("GITEA_DOCUMENT_INVALID", "research document ID is invalid") from exc if not isinstance(value, dict) or set(value) != { "project", "repositoryRef", "path", "ref", }: raise GiteaError("GITEA_DOCUMENT_INVALID", "research document ID is invalid") if not all(isinstance(item, str) for item in value.values()): raise GiteaError("GITEA_DOCUMENT_INVALID", "research document ID is invalid") if value["project"] != context.project_slug: raise GiteaError("GITEA_DOCUMENT_INVALID", "research document is outside the project") _validate_file_path(value["path"]) _validate_ref(value["ref"]) return value def _research_url(self, repository: RegisteredRepository, path: str, ref: str) -> str: base = self.client.settings.base_url.rstrip("/") return ( f"{base}/{quote(repository.owner, safe='')}/{quote(repository.name, safe='')}" f"/src/commit/{quote(ref, safe='')}/{quote(path, safe='/')}" ) @governed_tool("gitea.research.search") def research_search(self, context: ConnectorContext, query: str) -> dict[str, Any]: """Return citable, immutable repository documents using the standard search shape.""" normalized = _validate_text(query.strip(), field="query", minimum=1, maximum=500) self._refresh_discovered(context.project_id) terms = tuple( dict.fromkeys( part for part in re.findall(r"[a-z0-9_-]+", normalized.casefold()) if len(part) > 1 ) )[:12] repositories = self.registry.list(context.project_id)[:10] ranked: list[tuple[int, dict[str, str]]] = [] remaining_blob_budget = 30 important_names = { "AGENTS.md", "CLAUDE.md", "README.md", "README_NL.md", "SECURITY.md", "pyproject.toml", "package.json", } for repository in repositories: if remaining_blob_budget <= 0: break metadata = self.client.repository(repository) default_branch = str(metadata.get("default_branch", "")) if not default_branch: continue branch = self.client.branch(repository, default_branch) commit = branch.get("commit") if isinstance(branch, dict) else None ref = str(commit.get("id", "")) if isinstance(commit, dict) else "" if not _SHA.fullmatch(ref): continue tree_payload, _ = self._git_tree_cached(repository, ref) raw_tree = tree_payload.get("tree") files = [ item for item in (raw_tree if isinstance(raw_tree, list) else []) if isinstance(item, dict) and item.get("type") == "blob" and 0 <= int(item.get("size", 0) or 0) <= 200_000 ] files.sort( key=lambda item: ( -sum(term in str(item.get("path", "")).casefold() for term in terms), str(item.get("path", "")).split("/")[-1] not in important_names, str(item.get("path", "")), ) ) for candidate in files[: min(8, remaining_blob_budget)]: remaining_blob_budget -= 1 path = str(candidate.get("path", "")) sha = str(candidate.get("sha", "")) blob = self.client.git_blob(repository, sha) encoded = blob.get("content") if not isinstance(encoded, str): continue try: content = base64.b64decode(encoded.encode("ascii"), validate=False).decode( "utf-8" ) except (ValueError, UnicodeDecodeError, UnicodeEncodeError): continue filtered = safe_content(content) if filtered is None: continue haystack = f"{repository.qualified_name}\n{path}\n{filtered}".casefold() score = sum(haystack.count(term) for term in terms) if score <= 0: continue ranked.append( ( score, { "id": self._research_id(context, repository.repository_ref, path, ref), "title": f"{repository.qualified_name} · {path}"[:512], "url": self._research_url(repository, path, ref), }, ) ) ranked.sort(key=lambda item: (-item[0], item[1]["title"])) return {"results": [item for _, item in ranked[:20]]} @governed_tool("gitea.research.fetch") def research_fetch(self, context: ConnectorContext, identifier: str) -> dict[str, Any]: """Fetch one immutable search result using the standard fetch shape.""" document = self._decode_research_id(context, identifier) repository = self._repo(context, document["repositoryRef"]) result = self.file_read( context, f"itworx-git://{context.project_slug}/{repository.repository_ref}/{document['path']}", document["ref"], )["data"] return { "id": identifier, "title": f"{repository.qualified_name} · {document['path']}"[:512], "text": str(result["content"]), "url": self._research_url(repository, document["path"], document["ref"]), "metadata": { "repositoryRef": repository.repository_ref, "path": document["path"], "ref": document["ref"], "truncated": bool(result["truncated"]), }, } def _preview_changeset( self, context: ConnectorContext, repository_ref: str, branch: str, operations: list[dict[str, Any]], ) -> dict[str, Any]: candidate_branch = _validate_ref(branch) normalized = validate_operations(operations) repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) safe_branch = _validate_work_branch( candidate_branch, str(metadata.get("default_branch", "")) ) previews: list[dict[str, Any]] = [] total_diff = 0 for operation in normalized: kind = operation["kind"] path = "/".join(_validate_file_path(operation["path"])) destination = operation.get("destinationPath") if destination is not None: destination = "/".join(_validate_file_path(str(destination))) original = "" actual_sha: str | None = None if kind == "create": try: self.client.file(repository, tuple(path.split("/")), safe_branch) except GiteaError as exc: if exc.code != "GITEA_NOT_FOUND": raise else: raise GiteaError("GITEA_CHANGESET_CONFLICT", f"file already exists: {path}") else: payload = self.client.file(repository, tuple(path.split("/")), safe_branch) original, truncated, actual_sha = decode_file_payload(payload, 200_000) if truncated: raise GiteaError("GITEA_CHANGESET_INVALID", "source file exceeds budget") if actual_sha.casefold() != str(operation.get("expectedSha", "")).casefold(): raise GiteaError("GITEA_CHANGESET_CONFLICT", f"stale expectedSha for {path}") if destination is not None: try: self.client.file(repository, tuple(destination.split("/")), safe_branch) except GiteaError as exc: if exc.code != "GITEA_NOT_FOUND": raise else: raise GiteaError( "GITEA_CHANGESET_CONFLICT", f"destination already exists: {destination}", ) if kind == "create" or kind == "update": updated = _validate_text( str(operation["content"]), field="file content", minimum=0, maximum=200_000 ) elif kind == "patch": updated = apply_unified_patch(original, str(operation["patch"])) elif kind == "move": updated = original else: updated = "" diff = "".join( difflib.unified_diff( original.splitlines(keepends=True), updated.splitlines(keepends=True), fromfile=f"a/{path}", tofile=f"b/{destination or path}", n=3, ) ) encoded_diff = diff.encode("utf-8") total_diff += len(encoded_diff) previews.append( { "kind": kind, "path": path, "destinationPath": destination, "expectedSha": actual_sha, "diff": diff[:100_000], "diffTruncated": len(encoded_diff) > 100_000, } ) return self._envelope( context, { "repositoryRef": repository_ref, "branch": safe_branch, "valid": True, "operationCount": len(previews), "operations": previews, "totalDiffBytes": total_diff, }, source=f"gitea:{repository_ref}", truncated=any(item["diffTruncated"] for item in previews), ) @governed_tool("gitea.changeset.preview") def changeset_preview( self, context: ConnectorContext, repository_ref: str, branch: str, operations: list[dict[str, Any]], ) -> dict[str, Any]: return self._preview_changeset(context, repository_ref, branch, operations) @governed_tool("gitea.changeset.apply") def changeset_apply( self, context: ConnectorContext, repository_ref: str, branch: str, message: str, operations: list[dict[str, Any]], ) -> dict[str, Any]: preview = self._preview_changeset(context, repository_ref, branch, operations) safe_message = _validate_text(message, field="commit message", minimum=1, maximum=1_000) if self.changeset_workspace is None: raise GiteaError("GITEA_WRITES_DISABLED", "atomic changeset capability is not enabled") repository = self._repo(context, repository_ref) result = self.changeset_workspace.apply(repository, branch, safe_message, operations) self._invalidate_repository_cache(repository) result["previewedOperationCount"] = preview["data"]["operationCount"] return self._envelope(context, result, source=f"gitea:{repository_ref}") @governed_tool("gitea.repository.create") def repository_create( self, context: ConnectorContext, name: str, description: str = "", private: bool = True, auto_init: bool = True, default_branch: str = "main", ) -> dict[str, Any]: safe_name = name.strip() if not _SAFE_REPOSITORY_NAME.fullmatch(safe_name) or ".." in safe_name: raise GiteaError("GITEA_TARGET_INVALID", "repository name is outside the safe policy") safe_description = _validate_text( description, field="repository description", minimum=0, maximum=2_000 ) if not auto_init: raise GiteaError( "GITEA_REPOSITORY_INIT_REQUIRED", "governed repository creation requires an initialized default branch", ) safe_default_branch = _validate_ref(default_branch) if safe_default_branch != "main": raise GiteaError( "GITEA_DEFAULT_BRANCH_INVALID", "governed repository creation requires the canonical main branch", ) writer = self._writer() owner = self._repository_write_owner(context) registration_project_id, registration_project_slug = self.repository_create_project or ( context.project_id, context.project_slug, ) candidate = RegisteredRepository( repository_ref="temporary-created-repository", project_id=registration_project_id, owner=owner, name=safe_name, source_uri=(f"itworx-git://{registration_project_slug}/temporary-created-repository"), private=private, ) created = False try: payload = writer.repository(candidate) except GiteaError as error: if error.code != "GITEA_NOT_FOUND": raise payload = writer.create_repository( name=safe_name, description=safe_description, private=private, auto_init=auto_init, default_branch=safe_default_branch, ) created = True payload_owner = payload.get("owner") returned_owner = ( str(payload_owner.get("login", "")) if isinstance(payload_owner, dict) else "" ) returned_name = str(payload.get("name", "")) if ( returned_owner.casefold() != owner.casefold() or returned_name.casefold() != safe_name.casefold() ): raise GiteaError( "GITEA_INVALID_RESPONSE", "Gitea returned unexpected repository identity" ) if not created and bool(payload.get("private", False)) != private: raise GiteaError( "GITEA_REPOSITORY_CONFLICT", "an existing repository with this name has different visibility", ) repaired_empty_repository = False if bool(payload.get("empty", False)) or not str(payload.get("default_branch", "")): writer.create_file( candidate, ("README.md",), branch=safe_default_branch, content=( f"# {safe_name}\n\n" f"{safe_description or 'Repository initialized through governed Gitea MCP.'}\n" ), message="chore: initialize repository", ) repaired_empty_repository = True payload = writer.repository(candidate) actual_default_branch = str(payload.get("default_branch", safe_default_branch))[:300] if actual_default_branch != safe_default_branch: raise GiteaError( "GITEA_REPOSITORY_CONFLICT", "an existing repository with this name has a different default branch", ) branch_payload = writer.branch(candidate, safe_default_branch) branch_commit = branch_payload.get("commit") head_sha = str(branch_commit.get("id", "")) if isinstance(branch_commit, dict) else "" if not head_sha: raise GiteaError( "GITEA_REPOSITORY_NOT_READY", "Gitea did not return a usable default-branch head commit", ) self.registry.add_discovered(registration_project_id, registration_project_slug, [payload]) repository = next( ( item for item in self.registry.list(registration_project_id) if item.qualified_name.casefold() == f"{owner}/{safe_name}".casefold() ), None, ) if repository is None: raise GiteaError("GITEA_REGISTRATION_FAILED", "created repository was not registered") governance_context = context.model_copy( update={ "project_id": registration_project_id, "project_slug": registration_project_slug, } ) try: governance_preview = self._governance_plan_data( governance_context, repository.repository_ref ) governance_result = self.branch_protection_ensure( governance_context, repository.repository_ref, str(governance_preview["planHash"]), )["data"] except GiteaError as error: # Creation is already durable. A later governance failure must not # destroy a repository (or commits added by another caller). Keep # its registration so the unchanged idempotent request can recover. raise GiteaError( "GITEA_REPOSITORY_GOVERNANCE_PENDING", "Repository exists and is retained; governance setup is incomplete", category="configuration", upstream_status=error.upstream_status, expected_state="repository_governance_ready", actual_state="repository_retained_governance_pending", recovery_hint=( f"Do not create another repository. Repair governance access/configuration " f"for {owner}/{safe_name}, then repeat the unchanged " "repository-create request. " f"Cause: {error.code}." ), ) from error return self._envelope( context, { "repositoryRef": repository.repository_ref, "owner": owner, "name": returned_name, "fullName": f"{owner}/{returned_name}", "private": bool(payload.get("private", private)), "defaultBranch": actual_default_branch, "headSha": head_sha[:64], "initialized": True, "branchReady": True, "repairedEmptyRepository": repaired_empty_repository, "sourceUri": str(payload.get("html_url", repository.source_uri))[:2048], "created": created, "availableImmediately": True, "governanceReady": bool(governance_result.get("verified")), "managedValidationReady": bool(governance_result.get("compliant")), "defaultBranchProtected": bool(governance_result.get("compliant")), "governancePlanHash": str(governance_result.get("planHash", ""))[:64], }, source=f"gitea:{repository.repository_ref}", ) @governed_tool("gitea.branch.create") def branch_create( self, context: ConnectorContext, repository_ref: str, branch: str, from_ref: str, ) -> dict[str, Any]: candidate_branch = _validate_ref(branch) safe_from_ref = _validate_ref(from_ref) repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) safe_branch = _validate_work_branch( candidate_branch, str(metadata.get("default_branch", "")) ) if safe_branch == safe_from_ref: raise GiteaError("GITEA_TARGET_INVALID", "new and source branch must differ") payload = self._writer().create_branch(repository, safe_branch, safe_from_ref) data = { "repositoryRef": repository_ref, "name": str(payload.get("name", safe_branch))[:300], "commitSha": str((payload.get("commit") or {}).get("id", ""))[:64], "created": True, } return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.issue.create") def issue_create( self, context: ConnectorContext, repository_ref: str, title: str, body: str = "", ) -> dict[str, Any]: repository = self._repo(context, repository_ref) payload = self._writer().create_issue( repository, _validate_text(title, field="issue title", minimum=1, maximum=500), _validate_text(body, field="issue body", minimum=0, maximum=32_000), ) data = { "repositoryRef": repository_ref, "number": max(int(payload.get("number", 0)), 0), "title": safe_snippet(str(payload.get("title", title)), max_bytes=500), "state": str(payload.get("state", "open"))[:32], "sourceUri": str(payload.get("html_url", ""))[:2048], "created": True, } return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.issue.update") def issue_update( self, context: ConnectorContext, repository_ref: str, number: int, title: str | None = None, body: str | None = None, state: str | None = None, ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "issue number is invalid") if title is None and body is None and state is None: raise GiteaError("GITEA_INPUT_INVALID", "title, body or state must be provided") if state is not None and state not in {"open", "closed"}: raise GiteaError("GITEA_INPUT_INVALID", "issue state must be open or closed") repository = self._repo(context, repository_ref) current = self.client.issue(repository, number) if current.get("pull_request"): raise GiteaError( "GITEA_TARGET_INVALID", "pull requests must be changed with pull request tools", ) payload = self._writer().update_issue( repository, number, title=( _validate_text(title, field="issue title", minimum=1, maximum=500) if title is not None else None ), body=( _validate_text(body, field="issue body", minimum=0, maximum=32_000) if body is not None else None ), state=state, ) data = { "repositoryRef": repository_ref, "number": max(int(payload.get("number", number)), 0), "title": safe_snippet(str(payload.get("title", title or "")), max_bytes=500), "state": str(payload.get("state", state or current.get("state", "open")))[:32], "sourceUri": str(payload.get("html_url", current.get("html_url", "")))[:2048], "updated": True, } return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.issue.comment.create") def issue_comment_create( self, context: ConnectorContext, repository_ref: str, number: int, body: str, ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "issue number is invalid") repository = self._repo(context, repository_ref) payload = self._writer().create_issue_comment( repository, number, _validate_text(body, field="comment body", minimum=1, maximum=32_000), ) data = { "repositoryRef": repository_ref, "issueNumber": number, "commentId": max(int(payload.get("id", 0)), 0), "sourceUri": str(payload.get("html_url", ""))[:2048], "created": True, } return self._envelope(context, data, source=f"gitea:{repository_ref}") def _file_write( self, context: ConnectorContext, repository_ref: str, path: str, branch: str, content: str, message: str, *, expected_sha: str | None, ) -> dict[str, Any]: candidate_branch = _validate_ref(branch) safe_path = _validate_file_path(path) safe_content = _validate_text(content, field="file content", minimum=0, maximum=200_000) safe_message = _validate_text(message, field="commit message", minimum=1, maximum=1_000) if expected_sha is not None and not _SHA.fullmatch(expected_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected file SHA is invalid") repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) safe_branch = _validate_work_branch( candidate_branch, str(metadata.get("default_branch", "")) ) if expected_sha is None: payload = self._writer().create_file( repository, safe_path, branch=safe_branch, content=safe_content, message=safe_message, ) operation = "created" else: payload = self._writer().update_file( repository, safe_path, branch=safe_branch, content=safe_content, message=safe_message, sha=expected_sha, ) operation = "updated" content_payload = payload.get("content") commit_payload = payload.get("commit") content_result = content_payload if isinstance(content_payload, dict) else {} commit_result = commit_payload if isinstance(commit_payload, dict) else {} data = { "repositoryRef": repository_ref, "path": "/".join(safe_path), "branch": safe_branch, "contentSha": str(content_result.get("sha", ""))[:64], "commitSha": str(commit_result.get("sha", ""))[:64], "operation": operation, } self._invalidate_repository_cache(repository) return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.file.create") def file_create( self, context: ConnectorContext, repository_ref: str, path: str, branch: str, content: str, message: str, ) -> dict[str, Any]: return self._file_write( context, repository_ref, path, branch, content, message, expected_sha=None ) @governed_tool("gitea.file.update") def file_update( self, context: ConnectorContext, repository_ref: str, path: str, branch: str, content: str, message: str, expected_sha: str, ) -> dict[str, Any]: return self._file_write( context, repository_ref, path, branch, content, message, expected_sha=expected_sha, ) @governed_tool("gitea.file.delete") def file_delete( self, context: ConnectorContext, repository_ref: str, path: str, branch: str, message: str, expected_sha: str, ) -> dict[str, Any]: candidate_branch = _validate_ref(branch) safe_path = _validate_file_path(path) safe_message = _validate_text(message, field="commit message", minimum=1, maximum=1_000) if not _SHA.fullmatch(expected_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected file SHA is invalid") repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) safe_branch = _validate_work_branch( candidate_branch, str(metadata.get("default_branch", "")) ) payload = self._writer().delete_file( repository, safe_path, branch=safe_branch, message=safe_message, sha=expected_sha, ) commit_payload = payload.get("commit") commit_result = commit_payload if isinstance(commit_payload, dict) else {} self._invalidate_repository_cache(repository) return self._envelope( context, { "repositoryRef": repository_ref, "path": "/".join(safe_path), "branch": safe_branch, "commitSha": str(commit_result.get("sha", ""))[:64], "operation": "deleted", }, source=f"gitea:{repository_ref}", ) @governed_tool("gitea.validation.run") def validation_run( self, context: ConnectorContext, repository_ref: str, branch: str, profile: str, ) -> dict[str, Any]: if profile not in {"test", "lint", "typecheck", "build", "security", "full"}: raise GiteaError("GITEA_INPUT_INVALID", "validation profile is not allowlisted") repository = self._repo(context, repository_ref) safe_branch = _validate_ref(branch) workflows = self.client.action_workflows(repository) workflow = _select_validation_workflow(workflows) if workflow is None: raise GiteaError( "GITEA_VALIDATION_UNAVAILABLE", "repository has no allowlisted managed validation workflow", ) workflow_id = str(workflow.get("id", workflow.get("path", ""))) source_repository_id = max( int(workflow.get("source_repository_id", workflow.get("source_repo_id", 0)) or 0), 0 ) if not workflow_id or len(workflow_id) > 2_048: raise GiteaError("GITEA_INVALID_RESPONSE", "workflow id is invalid") branch_payload = self.client.branch(repository, safe_branch) branch_commit = branch_payload.get("commit") branch_head_sha = str( branch_commit.get("id", "") if isinstance(branch_commit, dict) else "" ) if not _SHA.fullmatch(branch_head_sha): raise GiteaError("GITEA_INVALID_RESPONSE", "branch head SHA is invalid") cache_key = (repository.qualified_name, branch_head_sha.casefold(), profile, workflow_id) # A single connector can receive the same retry from multiple MCP clients. # Serialize the small dispatch decision so those retries cannot create # duplicate workflow runs before the first response reaches the caller. with self._validation_dispatch_lock: now = time.monotonic() deduplication_source = "dispatch" for key, (created_at, _) in tuple(self._validation_dispatches.items()): if now - created_at >= _VALIDATION_REUSE_SECONDS: self._validation_dispatches.pop(key, None) cached = self._validation_dispatches.get(cache_key) if cached is not None: current = self._action_run_projection(self.client.action_run(repository, cached[1])) status = str(current.get("status", "")).casefold() conclusion = str(current.get("conclusion", "")).casefold() reusable = status != "completed" or conclusion == "success" if reusable and str(current.get("headSha", "")).casefold() == cache_key[1]: data = current dispatched = False deduplication_source = "memory" else: self._validation_dispatches.pop(cache_key, None) cached = None if cached is None: durable = self._find_reusable_validation_run( repository, safe_branch, branch_head_sha, profile, workflow ) if durable is not None: data = durable run_id = max(int(data.get("id", 0)), 0) if run_id: self._validation_dispatches[cache_key] = (now, run_id) dispatched = False deduplication_source = "gitea-actions" cached = (now, run_id) if cached is None: payload = self._writer().dispatch_action_workflow( repository, workflow_id, ref=safe_branch, inputs={"profile": profile}, source_repository_id=source_repository_id, ) data = self._action_run_projection(payload) run_id = max(int(data.get("id", 0)), 0) if run_id: actual_head = str(data.get("headSha", branch_head_sha)).casefold() actual_key = (repository.qualified_name, actual_head, profile, workflow_id) self._validation_dispatches[actual_key] = (now, run_id) dispatched = True data.update( { "repositoryRef": repository_ref, "branch": safe_branch, "profile": profile, "workflowId": workflow_id, "sourceRepositoryId": source_repository_id, "dispatched": dispatched, "reused": not dispatched, "deduplicatedRunId": None if dispatched else max(int(data.get("id", 0)), 0), "deduplicationSource": deduplication_source, "reuseWindowSeconds": _VALIDATION_REUSE_SECONDS, **_validation_polling(data, str(data.get("headSha", ""))), } ) return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.pullrequest.create") def pull_request_create( self, context: ConnectorContext, repository_ref: str, head: str, base: str, title: str, body: str = "", ) -> dict[str, Any]: repository = self._repo(context, repository_ref) safe_head = _validate_ref(head) safe_base = _validate_ref(base) if safe_head == safe_base: raise GiteaError("GITEA_TARGET_INVALID", "pull request head and base must differ") payload = self._writer().create_pull_request( repository, head=safe_head, base=safe_base, title=_validate_text(title, field="pull request title", minimum=1, maximum=500), body=_validate_text(body, field="pull request body", minimum=0, maximum=32_000), ) data = self._pull_request_projection(payload) data["repositoryRef"] = repository_ref data["created"] = True return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.pullrequest.update") def pull_request_update( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, title: str | None = None, body: str | None = None, ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") if not _SHA.fullmatch(expected_head_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected pull request head SHA is invalid") if title is None and body is None: raise GiteaError("GITEA_INPUT_INVALID", "title or body must be provided") repository = self._repo(context, repository_ref) current = self.client.pull_request(repository, number) head_value = current.get("head") head: dict[str, Any] = head_value if isinstance(head_value, dict) else {} _validate_ref(str(head.get("ref", ""))) if str(head.get("sha", "")) != expected_head_sha: raise GiteaError("GITEA_CONFLICT", "pull request head changed; refresh before updating") if str(current.get("state", "")) != "open": raise GiteaError("GITEA_TARGET_INVALID", "only an open pull request can be updated") payload = self._writer().update_pull_request( repository, number, title=( _validate_text(title, field="pull request title", minimum=1, maximum=500) if title is not None else None ), body=( _validate_text(body, field="pull request body", minimum=0, maximum=32_000) if body is not None else None ), ) data = self._pull_request_projection(payload) data.update({"repositoryRef": repository_ref, "updated": True}) return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.pullrequest.validation.ensure") def pull_request_validation_ensure( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, ) -> dict[str, Any]: """Reuse or retrigger a real full pull_request run for the exact PR head.""" if not 1 <= number <= 2_147_483_647 or not _SHA.fullmatch(expected_head_sha): raise GiteaError("GITEA_TARGET_INVALID", "PR number or expected head is invalid") repository = self._repo(context, repository_ref) pull = self.client.pull_request(repository, number) head = pull.get("head") or {} base = pull.get("base") or {} if pull.get("state") != "open" or pull.get("draft"): raise GiteaError("GITEA_TARGET_INVALID", "PR must be open and non-draft") if head.get("sha") != expected_head_sha: raise GiteaError("GITEA_CONFLICT", "PR head changed before validation ensure") default_branch = str(self.client.repository(repository).get("default_branch")) if str(base.get("ref", "")) != default_branch: raise GiteaError("GITEA_TARGET_INVALID", "PR does not target the default branch") protection = self.client.branch_protection(repository, str(base.get("ref", ""))) if not protection.get("enable_status_check") or MANAGED_VALIDATION_CONTEXT not in ( protection.get("status_check_contexts") or [] ): raise GiteaError("GITEA_PROTECTION_DRIFT", "canonical PR check is not required") def exact_runs() -> list[dict[str, Any]]: runs: list[dict[str, Any]] = [] for page in range(1, 6): batch = self.client.repository_action_runs(repository, page=page, limit=100) runs.extend(batch) if len(batch) < 100: break return sorted( ( self._action_run_projection(item) for item in runs if str(item.get("event", "")) == "pull_request" and str(item.get("head_sha", "")) == expected_head_sha and ( not isinstance(item.get("pull_requests"), list) or not item["pull_requests"] or any( int(pr.get("number", 0)) == number for pr in item["pull_requests"] if isinstance(pr, dict) ) ) ), key=lambda item: int(item.get("id", 0)), reverse=True, ) runs = exact_runs() statuses = self.client.commit_statuses(repository, expected_head_sha, 1, 100) current_status = next( (item for item in statuses if item.get("context") == MANAGED_VALIDATION_CONTEXT), None, ) for run in runs: run_id = int(run["id"]) if ( current_status and current_status.get("status") == "success" and re.search( rf"/actions/runs/{run_id}(?:/|$)", str(current_status.get("target_url", "")) ) and run.get("status") == "completed" and run.get("conclusion") == "success" and any( job.get("name") == "full" and job.get("conclusion") == "success" for job in self.client.action_run_jobs(repository, run_id) ) ): return self._envelope( context, { "repositoryRef": repository_ref, "pr": number, "headSha": expected_head_sha, "requiredContext": MANAGED_VALIDATION_CONTEXT, "state": "success", "runId": run_id, "event": "pull_request", "reused": True, }, source=f"gitea:{repository_ref}", ) if run.get("status") != "completed": return self._envelope( context, { "repositoryRef": repository_ref, "pr": number, "headSha": expected_head_sha, "requiredContext": MANAGED_VALIDATION_CONTEXT, "state": "pending", "runId": run_id, "event": "pull_request", "reused": True, }, source=f"gitea:{repository_ref}", ) break if runs: run_id = int(runs[0]["id"]) self._writer().rerun_action_run(repository, run_id) triggered = self.client.action_run(repository, run_id) if ( triggered.get("event") != "pull_request" or triggered.get("head_sha") != expected_head_sha ): raise GiteaError( "GITEA_PR_VALIDATION_REQUIRED", "rerun did not preserve the exact pull_request validation identity", ) trigger_method = "rerun_pull_request" else: # Gitea's default pull_request trigger includes reopened, but not edited. # Reopening creates a genuine PR event without changing the head commit. before_close = self.client.pull_request(repository, number) if ( before_close.get("state") != "open" or (before_close.get("head") or {}).get("sha") != expected_head_sha or (before_close.get("base") or {}).get("sha") != base.get("sha") ): raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "PR moved before validation trigger", ) self._writer().close_pull_request(repository, number) try: self._writer().reopen_pull_request(repository, number) except GiteaError as exc: raise GiteaError( "GITEA_PR_REOPEN_FAILED", "PR was closed but could not be reopened; retry the exact PR reopen", retryable=True, safe_to_retry=True, ) from exc trigger_method = "reopen_pull_request" run_id = 0 for _ in range(10): refreshed = self.client.pull_request(repository, number) if ( refreshed.get("state") != "open" or (refreshed.get("head") or {}).get("sha") != expected_head_sha ): raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "PR changed while canonical validation was triggered", ) found = exact_runs() if found: run_id = int(found[0]["id"]) break time.sleep(0.5) if not run_id: raise GiteaError( "GITEA_PR_RUN_NOT_VISIBLE", "PR reopened but its validation run is not visible yet; retry ensure", retryable=True, safe_to_retry=True, ) return self._envelope( context, { "repositoryRef": repository_ref, "pr": number, "headSha": expected_head_sha, "requiredContext": MANAGED_VALIDATION_CONTEXT, "state": "pending", "runId": run_id, "event": "pull_request", "reused": False, "triggerMethod": trigger_method, }, source=f"gitea:{repository_ref}", ) @governed_tool("gitea.pullrequest.close") def pull_request_close( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") if not _SHA.fullmatch(expected_head_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected pull request head SHA is invalid") repository = self._repo(context, repository_ref) current = self.client.pull_request(repository, number) head_value = current.get("head") head: dict[str, Any] = head_value if isinstance(head_value, dict) else {} _validate_ref(str(head.get("ref", ""))) if str(head.get("sha", "")) != expected_head_sha: raise GiteaError("GITEA_CONFLICT", "pull request head changed; refresh before closing") if str(current.get("state", "")) != "open": raise GiteaError("GITEA_TARGET_INVALID", "only an open pull request can be closed") payload = self._writer().close_pull_request(repository, number) data = self._pull_request_projection(payload) data.update({"repositoryRef": repository_ref, "closed": True}) return self._envelope(context, data, source=f"gitea:{repository_ref}") @governed_tool("gitea.pullrequest.sync") def pull_request_sync( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, expected_base_sha: str, strategy: str, dry_run: bool, ) -> dict[str, Any]: """Merge or rebase the exact current base into a PR branch without inventing conflicts.""" if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") if not _SHA.fullmatch(expected_head_sha) or not _SHA.fullmatch(expected_base_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected pull request SHAs are invalid") if strategy not in {"merge", "rebase"}: raise GiteaError("GITEA_INPUT_INVALID", "sync strategy must be merge or rebase") repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) current = self._pull_request_projection(self.client.pull_request(repository, number)) head_ref = _validate_ref(str(current.get("headRef", ""))) if head_ref == str(metadata.get("default_branch", "")): raise GiteaError("GITEA_TARGET_INVALID", "the default branch cannot be a sync target") if current.get("state") != "open": raise GiteaError("GITEA_TARGET_INVALID", "only an open pull request can be synced") if current.get("headSha") != expected_head_sha: raise GiteaError( "GITEA_CONFLICT", "pull request head changed; refresh before syncing", expected_state=expected_head_sha, actual_state=str(current.get("headSha", "")), recovery_hint="Call gitea.change.reconcile and retry with the returned head SHA.", ) if current.get("baseSha") != expected_base_sha: raise GiteaError( "GITEA_CONFLICT", "pull request base changed; refresh before syncing", expected_state=expected_base_sha, actual_state=str(current.get("baseSha", "")), recovery_hint="Call gitea.change.reconcile and retry with the returned base SHA.", ) comparison_payload = self.client.compare(repository, expected_base_sha, expected_head_sha) reverse_comparison_payload = self.client.compare( repository, expected_head_sha, expected_base_sha ) comparison = self._compact_comparison( comparison_payload, base=str(current.get("baseRef", "")), head=head_ref, reverse_payload=reverse_comparison_payload, ) preview = { "repositoryRef": repository_ref, "number": number, "branch": head_ref, "strategy": strategy, "dryRun": dry_run, "expectedHeadSha": expected_head_sha, "expectedBaseSha": expected_base_sha, "mergeable": current.get("mergeable"), "behindBy": comparison["behindBy"], "conflictFiles": [], "conflictDetailsAvailable": False, "wouldChange": bool(comparison["behindBy"] or current.get("mergeable") is False), "validationRequired": False, "synced": False, } if dry_run: return self._envelope(context, preview, source=f"gitea:{repository_ref}") # Re-read immediately before the non-conditional Gitea update API. The # upstream endpoint has no If-Match/SHA argument, so this second fence # closes all connector-side work between check and use and fails closed # on state observed to have moved. External writers can still race the # upstream endpoint itself; expose that limitation as optimistic # concurrency instead of claiming an atomic precondition. before_update = self._pull_request_projection(self.client.pull_request(repository, number)) if ( before_update.get("headSha") != expected_head_sha or before_update.get("baseSha") != expected_base_sha ): raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "pull request state moved while synchronization was being prepared", retryable=True, safe_to_retry=True, expected_state=f"{expected_head_sha}:{expected_base_sha}", actual_state=( f"{before_update.get('headSha', '')}:{before_update.get('baseSha', '')}" ), recovery_hint="Call gitea.change.reconcile and request fresh confirmation.", ) self._writer().update_pull_request_branch(repository, number, style=strategy) self._invalidate_repository_cache(repository) updated = self._pull_request_projection(self.client.pull_request(repository, number)) new_head_sha = str(updated.get("headSha", "")) return self._envelope( context, { **preview, "dryRun": False, "newHeadSha": new_head_sha, "newBaseSha": updated.get("baseSha"), "mergeable": updated.get("mergeable"), "validationRequired": new_head_sha != expected_head_sha, "synced": True, "preconditionMode": "optimistic-double-read", }, source=f"gitea:{repository_ref}", ) def _require_merge_eligible( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, ) -> tuple[RegisteredRepository, str, int, str]: """Authoritative server-side PR, protection, review and exact-run gate.""" repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) current = self.client.pull_request(repository, number) head_value = current.get("head") base_value = current.get("base") head: dict[str, Any] = head_value if isinstance(head_value, dict) else {} base: dict[str, Any] = base_value if isinstance(base_value, dict) else {} _validate_ref(str(head.get("ref", ""))) if str(head.get("sha", "")) != expected_head_sha: raise GiteaError("GITEA_CONFLICT", "pull request head changed; refresh before merging") if str(base.get("ref", "")) != str(metadata.get("default_branch", "")): raise GiteaError( "GITEA_TARGET_INVALID", "pull request must target the repository default branch" ) if str(current.get("state", "")) != "open" or bool(current.get("draft", False)): raise GiteaError("GITEA_TARGET_INVALID", "pull request must be open and non-draft") if current.get("mergeable") is not True: raise GiteaError("GITEA_CONFLICT", "pull request is not currently mergeable") protection = self.client.branch_protection(repository, str(base.get("ref", ""))) required_contexts = set((protection or {}).get("status_check_contexts") or []) if MANAGED_VALIDATION_CONTEXT not in required_contexts: raise GiteaError( "GITEA_PROTECTION_DRIFT", "the default branch does not require the canonical full pull_request context", ) if protection.get("enable_push") or protection.get("enable_force_push"): raise GiteaError( "GITEA_PROTECTION_DRIFT", "the default branch permits direct or force pushes", ) base_sha = str(base.get("sha", "")) current_base = self.client.branch(repository, str(base.get("ref", ""))) current_base_sha = str((current_base.get("commit") or {}).get("id", "")) if not _SHA.fullmatch(base_sha) or current_base_sha != base_sha: raise GiteaError( "GITEA_BASE_CHANGED", "the default branch changed; refresh and synchronize the pull request", expected_state=base_sha, actual_state=current_base_sha, ) if protection.get("block_on_outdated_branch"): behind = self.client.compare(repository, expected_head_sha, current_base_sha) if int(behind.get("total_commits", 0) or 0) > 0: raise GiteaError( "GITEA_BRANCH_OUTDATED", "the pull request branch is behind the protected default branch", ) reviews = self.client.pull_request_reviews(repository, number, 1, 100) if len(reviews) == 100: raise GiteaError("GITEA_REVIEW_TRUNCATED", "review history exceeds the safe bound") latest_reviews: dict[str, dict[str, Any]] = {} for review in sorted( reviews, key=lambda item: (str(item.get("submitted_at", "")), int(item.get("id", 0))), ): reviewer = str((review.get("user") or {}).get("login", "")).casefold() if reviewer: latest_reviews[reviewer] = review if protection.get("block_on_rejected_reviews") and any( str(item.get("state", "")).upper() in {"REQUEST_CHANGES", "REJECTED"} for item in latest_reviews.values() ): raise GiteaError("GITEA_BLOCKING_REVIEW", "a reviewer requested changes") required_approvals = max(int(protection.get("required_approvals", 0) or 0), 0) approval_count = sum( str(item.get("state", "")).upper() == "APPROVED" for item in latest_reviews.values() ) if approval_count < required_approvals: raise GiteaError( "GITEA_APPROVAL_REQUIRED", f"{required_approvals} approvals required; {approval_count} current", ) statuses = self.client.commit_statuses(repository, expected_head_sha, 1, 100) if len(statuses) == 100: raise GiteaError( "GITEA_STATUS_TRUNCATED", "commit status history exceeds the safe bound" ) for required_context in sorted(required_contexts - {MANAGED_VALIDATION_CONTEXT}): required_status = next( (item for item in statuses if item.get("context") == required_context), None ) if required_status is None or required_status.get("status") != "success": raise GiteaError( "GITEA_REQUIRED_CHECK_BLOCKED", f"required status {required_context!r} is not successful on the exact head", ) canonical_status = next( (item for item in statuses if item.get("context") == MANAGED_VALIDATION_CONTEXT), None, ) if canonical_status is None or canonical_status.get("status") != "success": raise GiteaError( "GITEA_PR_CHECK_MISSING" if canonical_status is None else "GITEA_PR_CHECK_FAILED" if canonical_status.get("status") in {"failure", "error"} else "GITEA_PR_CHECK_PENDING", "the canonical full pull_request status is not successful on the exact head", ) run_url = str(canonical_status.get("target_url", "")) run_match = re.search(r"/actions/runs/([1-9][0-9]*)(?:/|$)", run_url) if run_match is None: raise GiteaError( "GITEA_PR_VALIDATION_REQUIRED", "the required status has no verifiable canonical validation run", ) run = self._action_run_projection(self.client.action_run(repository, int(run_match[1]))) workflow = _select_validation_workflow(self.client.action_workflows(repository)) workflow_id = str(run.get("workflowId", "")).strip().casefold() identities = self._workflow_identity_values(workflow or {}) jobs = self.client.action_run_jobs(repository, int(run_match[1])) full_job = next((job for job in jobs if job.get("name") == "full"), None) if ( int(run.get("id", 0)) != int(run_match[1]) or str(run.get("headSha", "")) != expected_head_sha or str(run.get("event", "")) != "pull_request" or str(run.get("status", "")) != "completed" or str(run.get("conclusion", "")) != "success" or ( workflow_id and workflow_id not in identities and workflow_id.rsplit("/", 1)[-1] not in identities ) or not isinstance(full_job, dict) or full_job.get("status") != "completed" or full_job.get("conclusion") != "success" ): raise GiteaError( "GITEA_PR_VALIDATION_REQUIRED", "the required status does not identify a successful canonical " "pull_request run on the exact head", ) return repository, base_sha, int(run_match[1]), str(base.get("ref", "")) @governed_tool("gitea.pullrequest.merge") def pull_request_merge( self, context: ConnectorContext, repository_ref: str, number: int, expected_head_sha: str, method: str = "squash", ) -> dict[str, Any]: if not 1 <= number <= 2_147_483_647: raise GiteaError("GITEA_TARGET_INVALID", "pull request number is invalid") if not _SHA.fullmatch(expected_head_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected pull request head SHA is invalid") if method not in {"merge", "rebase", "rebase-merge", "squash", "fast-forward-only"}: raise GiteaError("GITEA_INPUT_INVALID", "merge method is not allowlisted") repository, base_sha, run_id, base_ref = self._require_merge_eligible( context, repository_ref, number, expected_head_sha ) before_merge = self.client.pull_request(repository, number) before_head = before_merge.get("head") or {} before_base = before_merge.get("base") or {} latest_base = self.client.branch(repository, base_ref) if ( before_head.get("sha") != expected_head_sha or before_base.get("sha") != base_sha or str((latest_base.get("commit") or {}).get("id", "")) != base_sha ): raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "pull request head or protected base changed before merge", ) try: self._writer().merge_pull_request( repository, number, method=method, head_commit_id=expected_head_sha ) except GiteaError as exc: if exc.code == "GITEA_STATE_TRANSITION_DENIED": raise GiteaError( "GITEA_UPSTREAM_MERGE_REJECTED", "Gitea rejected the guarded merge after eligibility verification; " "refresh the PR and required checks", ) from exc raise self._invalidate_repository_cache(repository) merged = self._pull_request_projection(self.client.pull_request(repository, number)) merge_sha = str(merged.get("mergeCommitSha", "")) resulting_main_sha = str( (self.client.branch(repository, base_ref).get("commit") or {}).get("id", "") ) if not merged.get("merged") or not _SHA.fullmatch(resulting_main_sha): raise GiteaError( "GITEA_MERGE_RESULT_UNVERIFIED", "Gitea accepted the merge but the resulting default branch could not be verified", ) merged.update( { "repositoryRef": repository_ref, "mergeMethod": method, "validationRunId": run_id, "resultingMainSha": resulting_main_sha, "merged": True, "auditEvidence": { "repositoryRef": repository_ref, "pr": number, "headSha": expected_head_sha, "baseSha": base_sha, "requiredContext": MANAGED_VALIDATION_CONTEXT, "validationRunId": run_id, "validationEvent": "pull_request", "validationConclusion": "success", "mergeMethod": method, "mergeCommitSha": merge_sha, "resultingMainSha": resulting_main_sha, "occurredAt": datetime.now(UTC).isoformat(), }, } ) return self._envelope(context, merged, source=f"gitea:{repository_ref}") @governed_tool("gitea.branch.delete") def branch_delete( self, context: ConnectorContext, repository_ref: str, branch: str, expected_head_sha: str, ) -> dict[str, Any]: safe_branch = _validate_ref(branch) if not _SHA.fullmatch(expected_head_sha): raise GiteaError("GITEA_TARGET_INVALID", "expected branch head SHA is invalid") repository = self._repo(context, repository_ref) metadata = self.client.repository(repository) if safe_branch == str(metadata.get("default_branch", "")): raise GiteaError("GITEA_TARGET_INVALID", "the default branch cannot be deleted") current = self.client.branch(repository, safe_branch) current_sha = str((current.get("commit") or {}).get("id", "")) if current_sha != expected_head_sha: raise GiteaError("GITEA_CONFLICT", "branch head changed; refresh before deleting") merged_pull, closed_pull = cleanup_basis( self.client, repository, safe_branch, expected_head_sha ) before_delete = self.client.branch(repository, safe_branch) before_delete_sha = str((before_delete.get("commit") or {}).get("id", "")) if before_delete_sha != expected_head_sha: raise GiteaError( "GITEA_CONCURRENT_MODIFICATION", "branch head moved while cleanup was being prepared", expected_state=expected_head_sha, actual_state=before_delete_sha, recovery_hint="Refresh the branch and request fresh cleanup confirmation.", ) merged_pull, closed_pull = cleanup_basis( self.client, repository, safe_branch, expected_head_sha ) cleanup_disposition = "merged" if merged_pull is not None else "closed-unmerged" self._writer().delete_branch(repository, safe_branch) self._invalidate_repository_cache(repository) return self._envelope( context, { "repositoryRef": repository_ref, "branch": safe_branch, "deletedHeadSha": expected_head_sha, "mergedPullRequest": ( max(int(merged_pull.get("number", 0)), 0) if merged_pull is not None else 0 ), "closedPullRequest": ( max(int(closed_pull.get("number", 0)), 0) if closed_pull is not None else 0 ), "cleanupDisposition": cleanup_disposition, "preconditionMode": "optimistic-double-read", "deleted": True, }, source=f"gitea:{repository_ref}", ) def tools(self) -> tuple[ToolPublication, ...]: return GITEA_TOOLS def write_tools(self) -> tuple[ToolPublication, ...]: """Return feature-gated PR-first publications for a controlled-operations release.""" return GITEA_PR_WRITE_TOOLS + GITEA_LIFECYCLE_WRITE_TOOLS def resources(self) -> tuple[ResourcePublication, ...]: return GITEA_RESOURCES def _tool_handlers(self) -> dict[str, Callable[..., dict[str, Any]]]: return { "gitea.capabilities.get": self.capabilities_get, "gitea.connection.check": self.connection_check, "gitea.selftest.run": self.selftest_run, "gitea.repository.list": self.repository_list, "gitea.repository.get": self.repository_get, "gitea.repository.context": self.repository_context, "gitea.repository.health": self.repository_health, "gitea.branchprotection.get": self.branch_protection_get, "gitea.branchprotection.preview": self.branch_protection_preview, "gitea.branchprotection.ensure": self.branch_protection_ensure, "gitea.branch.list": self.branch_list, "gitea.commit.list": self.commit_list, "gitea.commit.get": self.commit_get, "gitea.ref.compare": self.ref_compare, "gitea.release.list": self.release_list, "gitea.issue.list": self.issue_list, "gitea.issue.search": self.issue_search, "gitea.pullrequest.list": self.pull_request_list, "gitea.pullrequest.get": self.pull_request_get, "gitea.pullrequest.feedback": self.pull_request_feedback, "gitea.commitstatus.list": self.commit_status_list, "gitea.file.read": self.file_read, "gitea.tree.list": self.tree_list, "gitea.code.search": self.code_search, "gitea.research.search": self.research_search, "gitea.research.fetch": self.research_fetch, "gitea.changeset.preview": self.changeset_preview, "gitea.validation.get": self.validation_get, "gitea.validation.failure.get": self.validation_failure_get, "gitea.validation.list": self.validation_list, "gitea.change.status": self.change_status, "gitea.change.review": self.change_review, "gitea.change.reconcile": self.change_reconcile, "gitea.workspace.status": self.workspace_status, "gitea.portfolio.health": self.portfolio_health, "gitea.portfolio.governance.preview": self.portfolio_governance_preview, "gitea.portfolio.governance.apply": self.portfolio_governance_apply, "gitea.portfolio.governance.verify": self.portfolio_governance_verify, "gitea.orchestration.plan": self.orchestration_plan, "gitea.repository.create": self.repository_create, "gitea.branch.create": self.branch_create, "gitea.issue.create": self.issue_create, "gitea.issue.update": self.issue_update, "gitea.issue.comment.create": self.issue_comment_create, "gitea.file.create": self.file_create, "gitea.file.update": self.file_update, "gitea.file.delete": self.file_delete, "gitea.changeset.apply": self.changeset_apply, "gitea.validation.run": self.validation_run, "gitea.workspace.run": self.workspace_run, "gitea.orchestration.start": self.orchestration_start, "gitea.pullrequest.create": self.pull_request_create, "gitea.pullrequest.update": self.pull_request_update, "gitea.pullrequest.close": self.pull_request_close, "gitea.pullrequest.validation.ensure": self.pull_request_validation_ensure, "gitea.pullrequest.sync": self.pull_request_sync, "gitea.pullrequest.merge": self.pull_request_merge, "gitea.branch.delete": self.branch_delete, } def invoke(self, tool: str, context: ConnectorContext, **arguments: Any) -> dict[str, Any]: method = self._tool_handlers().get(tool) if method is None: raise ValueError("unknown gitea tool") return method(context, **arguments) def health(self) -> dict[str, Any]: return { "status": "healthy", "component": "gitea-connector", "readOnly": self.write_client is None, "writeMode": "pr-first" if self.write_client is not None else "disabled", "atomicChangesets": self.changeset_workspace is not None, "discoveryProjects": len(self.discovery_projects), "discoveryCacheSeconds": _DISCOVERY_CACHE_SECONDS, } def readiness(self) -> dict[str, Any]: now = time.monotonic() with self._readiness_lock: if self._readiness_cache is not None and now - self._readiness_cache[0] < 30: return copy.deepcopy(self._readiness_cache[1]) repositories = self.client.accessible_private_repositories(1, 1) if not repositories: raise GiteaError("GITEA_NOT_READY", "Gitea discovery returned no repositories") publications = list(self.tools()) if self.write_client is not None: publications.extend(self.write_tools()) descriptors = sorted( [ { "name": item.canonical_name, "version": item.version, "riskClass": item.risk_class, "inputSchema": item.input_schema, } for item in publications ], key=lambda item: str(item["name"]), ) runtime = runtime_snapshot() snapshot = { "status": "ready", "component": "gitea-connector", "readOnly": self.write_client is None, "writeMode": "pr-first" if self.write_client is not None else "disabled", "atomicChangesets": self.changeset_workspace is not None, "upstream": "reachable", "toolCount": len(publications), "capabilityFingerprint": hashlib.sha256( json.dumps(descriptors, sort_keys=True, separators=(",", ":")).encode() ).hexdigest(), "probeCacheSeconds": 30, "runtime": runtime, "subsystems": { "runtime": "READY", "dispatcher": "READY", "worker": ("READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED"), "session": "RESUMABLE", "upstream_gitea": "READY", }, } with self._readiness_lock: self._readiness_cache = (now, copy.deepcopy(snapshot)) return snapshot _PAGE_INPUT: dict[str, Any] = { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "cursor": {"type": ["string", "null"], "maxLength": 500}, }, "required": ["repositoryRef"], } _PAGE_OUTPUT: dict[str, Any] = {"type": "object", "additionalProperties": False} _CHANGESET_OPERATION_SCHEMA: dict[str, Any] = { "type": "object", "additionalProperties": False, "properties": { "kind": {"type": "string", "enum": ["create", "update", "delete", "move", "patch"]}, "path": {"type": "string", "minLength": 1, "maxLength": 2_048}, "destinationPath": {"type": "string", "minLength": 1, "maxLength": 2_048}, "content": {"type": "string", "maxLength": 200_000}, "patch": {"type": "string", "maxLength": 300_000}, "expectedSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, }, "required": ["kind", "path"], } def _changeset_schema(*, include_message: bool) -> dict[str, Any]: properties: dict[str, Any] = { "repositoryRef": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$"}, "branch": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$", "maxLength": 300, }, "operations": { "type": "array", "minItems": 1, "maxItems": 50, "items": _CHANGESET_OPERATION_SCHEMA, }, } required = ["repositoryRef", "branch", "operations"] if include_message: properties["message"] = {"type": "string", "minLength": 1, "maxLength": 1_000} required.append("message") return { "type": "object", "additionalProperties": False, "properties": properties, "required": required, } def _tool_input_schema(name: str) -> dict[str, Any]: if name in {"gitea.capabilities.get", "gitea.connection.check"}: return {"type": "object", "additionalProperties": False, "properties": {}} if name == "gitea.selftest.run": return { "type": "object", "additionalProperties": False, "properties": { "mode": { "type": "string", "enum": ["read", "guarded-write"], "default": "read", } }, } if name == "gitea.research.search": return { "type": "object", "additionalProperties": False, "properties": {"query": {"type": "string", "minLength": 1, "maxLength": 500}}, "required": ["query"], } if name == "gitea.research.fetch": return { "type": "object", "additionalProperties": False, "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 4096}}, "required": ["id"], } if name in { "gitea.portfolio.health", "gitea.portfolio.governance.preview", "gitea.portfolio.governance.verify", }: return { "type": "object", "additionalProperties": False, "properties": { "repositoryRefs": { "type": "array", "minItems": 1, "maxItems": 25, "uniqueItems": True, "items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$"}, } }, "required": ["repositoryRefs"], } if name == "gitea.orchestration.plan": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRefs": { "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": True, "items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$"}, }, "branchSuffix": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,79}$", }, }, "required": ["repositoryRefs", "branchSuffix"], } if name == "gitea.repository.list": return { "type": "object", "additionalProperties": False, "properties": { "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "cursor": {"type": ["string", "null"], "maxLength": 500}, }, } if name == "gitea.issue.search": return { "type": "object", "additionalProperties": False, "properties": { "state": { "type": "string", "enum": ["open", "closed", "all"], "default": "open", }, "excludeRepositoryNames": { "type": "array", "maxItems": 20, "uniqueItems": True, "items": {"type": "string", "minLength": 1, "maxLength": 120}, "default": [], }, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "cursor": {"type": ["string", "null"], "maxLength": 500}, }, } if name in { "gitea.repository.get", "gitea.repository.health", "gitea.branchprotection.get", "gitea.branchprotection.preview", }: return { "type": "object", "additionalProperties": False, "properties": {"repositoryRef": {"type": "string"}}, "required": ["repositoryRef"], } if name == "gitea.repository.context": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "ref": {"type": ["string", "null"], "maxLength": 300}, }, "required": ["repositoryRef"], } if name == "gitea.commit.get": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "ref": {"type": "string", "minLength": 1, "maxLength": 300}, }, "required": ["repositoryRef", "ref"], } if name == "gitea.ref.compare": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "base": {"type": "string", "minLength": 1, "maxLength": 300}, "head": {"type": "string", "minLength": 1, "maxLength": 300}, }, "required": ["repositoryRef", "base", "head"], } if name == "gitea.file.read": return { "type": "object", "additionalProperties": False, "properties": { "fileUri": {"type": "string", "maxLength": 2048}, "ref": {"type": ["string", "null"], "maxLength": 300}, }, "required": ["fileUri"], } if name == "gitea.code.search": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "query": {"type": "string", "minLength": 1, "maxLength": 500}, "ref": {"type": ["string", "null"], "maxLength": 300}, "pathPrefix": {"type": "string", "maxLength": 2_048, "default": ""}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "cursor": {"type": ["string", "null"], "maxLength": 500}, }, "required": ["repositoryRef", "query"], } if name in {"gitea.pullrequest.get", "gitea.pullrequest.feedback"}: return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, }, "required": ["repositoryRef", "number"], } if name == "gitea.changeset.preview": return _changeset_schema(include_message=False) if name in {"gitea.validation.get", "gitea.validation.failure.get"}: validation_properties: dict[str, Any] = { "repositoryRef": {"type": "string"}, "runId": {"type": "integer", "minimum": 1}, } if name == "gitea.validation.get": validation_properties["includeLogs"] = {"type": "boolean", "default": False} return { "type": "object", "additionalProperties": False, "properties": validation_properties, "required": ["repositoryRef", "runId"], } if name in { "gitea.change.status", "gitea.change.review", "gitea.change.reconcile", "gitea.workspace.status", }: return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "branch": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$", }, }, "required": ["repositoryRef", "branch"], } if name == "gitea.validation.list": validation_list_properties = dict(_PAGE_INPUT["properties"]) validation_list_properties["branch"] = { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$", } return { "type": "object", "additionalProperties": False, "properties": validation_list_properties, "required": ["repositoryRef", "branch"], } if name == "gitea.tree.list": return { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": {"type": "string"}, "path": {"type": "string", "maxLength": 2048, "default": ""}, "ref": {"type": ["string", "null"], "maxLength": 300}, }, "required": ["repositoryRef"], } if name == "gitea.commitstatus.list": commit_status_properties: dict[str, Any] = dict(_PAGE_INPUT["properties"]) commit_status_properties["ref"] = { "type": "string", "minLength": 1, "maxLength": 300, } schema: dict[str, Any] = { "type": "object", "additionalProperties": False, "properties": commit_status_properties, "required": ["repositoryRef", "ref"], } return schema return _PAGE_INPUT GITEA_READ_TOOLS = tuple( ToolPublication( canonical_name=name, version="1.12.0" if name in { "gitea.selftest.run", "gitea.validation.failure.get", "gitea.change.reconcile", } else "1.9.0" if name in { "gitea.changeset.preview", "gitea.validation.list", "gitea.change.status", "gitea.change.review", "gitea.workspace.status", "gitea.branchprotection.preview", "gitea.portfolio.governance.preview", "gitea.portfolio.governance.verify", } else "1.8.0" if name == "gitea.branchprotection.get" else "1.5.0", connector_type="gitea", risk_class="R1", input_schema=_tool_input_schema(name), output_schema=_PAGE_OUTPUT, required_scopes=("project:{project}:read",), ) for name in ( "gitea.capabilities.get", "gitea.connection.check", "gitea.selftest.run", "gitea.repository.list", "gitea.repository.get", "gitea.repository.context", "gitea.repository.health", "gitea.branchprotection.get", "gitea.branchprotection.preview", "gitea.branch.list", "gitea.commit.list", "gitea.commit.get", "gitea.ref.compare", "gitea.release.list", "gitea.issue.list", "gitea.issue.search", "gitea.pullrequest.list", "gitea.pullrequest.get", "gitea.pullrequest.feedback", "gitea.commitstatus.list", "gitea.file.read", "gitea.tree.list", "gitea.code.search", "gitea.research.search", "gitea.research.fetch", "gitea.changeset.preview", "gitea.validation.get", "gitea.validation.failure.get", "gitea.validation.list", "gitea.change.status", "gitea.change.review", "gitea.change.reconcile", "gitea.workspace.status", "gitea.portfolio.health", "gitea.portfolio.governance.preview", "gitea.portfolio.governance.verify", "gitea.orchestration.plan", ) ) def _write_tool_input_schema(name: str) -> dict[str, Any]: if name == "gitea.repository.create": return { "type": "object", "additionalProperties": False, "properties": { "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$", }, "description": {"type": "string", "maxLength": 2_000}, "private": {"type": "boolean"}, "autoInit": { "type": "boolean", "const": True, "description": ( "Must be true so the default branch exists before the tool returns." ), }, "defaultBranch": { "type": "string", "const": "main", }, }, "required": ["name", "description", "private", "autoInit", "defaultBranch"], } properties: dict[str, Any] = { "repositoryRef": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$", "description": "Opaque repositoryRef returned by gitea.repository.list.", } } required = ["repositoryRef"] change_branch = { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$", "maxLength": 300, } safe_ref = { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,299}$", "maxLength": 300, } if name == "gitea.branchprotection.ensure": properties["expectedPlanHash"] = {"type": "string", "pattern": "^[0-9a-f]{64}$"} properties["applyScope"] = { "type": "string", "enum": ["all", "protection_only"], "default": "all", } required.append("expectedPlanHash") elif name == "gitea.portfolio.governance.apply": return { "type": "object", "additionalProperties": False, "properties": { "plans": { "type": "array", "minItems": 1, "maxItems": _GOVERNANCE_BATCH_LIMIT, "uniqueItems": True, "items": { "type": "object", "additionalProperties": False, "properties": { "repositoryRef": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$", }, "planHash": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": ( "Use planHash for all scope or protectionOnlyPlanHash " "for protection_only scope." ), }, "applyScope": { "type": "string", "enum": ["all", "protection_only"], "default": "all", }, }, "required": ["repositoryRef", "planHash"], }, } }, "required": ["plans"], } elif name == "gitea.branch.create": properties.update({"branch": change_branch, "fromRef": safe_ref}) required.extend(("branch", "fromRef")) elif name == "gitea.issue.create": properties.update( { "title": {"type": "string", "minLength": 1, "maxLength": 500}, "body": {"type": "string", "maxLength": 32_000, "default": ""}, } ) required.append("title") elif name == "gitea.issue.update": properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "title": {"type": ["string", "null"], "minLength": 1, "maxLength": 500}, "body": {"type": ["string", "null"], "maxLength": 32_000}, "state": {"type": ["string", "null"], "enum": ["open", "closed", None]}, } ) required.append("number") elif name == "gitea.issue.comment.create": properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "body": {"type": "string", "minLength": 1, "maxLength": 32_000}, } ) required.extend(("number", "body")) elif name in {"gitea.file.create", "gitea.file.update", "gitea.file.delete"}: properties.update( { "path": {"type": "string", "minLength": 1, "maxLength": 2_048}, "branch": change_branch, "message": {"type": "string", "minLength": 1, "maxLength": 1_000}, } ) required.extend(("path", "branch", "message")) if name != "gitea.file.delete": properties["content"] = {"type": "string", "maxLength": 200_000} required.append("content") if name in {"gitea.file.update", "gitea.file.delete"}: properties["expectedSha"] = { "type": "string", "pattern": "^[0-9a-fA-F]{40,64}$", } required.append("expectedSha") elif name == "gitea.pullrequest.create": properties.update( { "head": change_branch, "base": safe_ref, "title": {"type": "string", "minLength": 1, "maxLength": 500}, "body": {"type": "string", "maxLength": 32_000, "default": ""}, } ) required.extend(("head", "base", "title")) elif name == "gitea.pullrequest.update": properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "expectedHeadSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, "title": {"type": ["string", "null"], "minLength": 1, "maxLength": 500}, "body": {"type": ["string", "null"], "maxLength": 32_000}, } ) required.extend(("number", "expectedHeadSha")) elif name in {"gitea.pullrequest.close", "gitea.pullrequest.validation.ensure"}: properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "expectedHeadSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, } ) required.extend(("number", "expectedHeadSha")) elif name == "gitea.pullrequest.sync": properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "expectedHeadSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, "expectedBaseSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, "strategy": {"type": "string", "enum": ["merge", "rebase"]}, "dryRun": {"type": "boolean"}, } ) required.extend(("number", "expectedHeadSha", "expectedBaseSha", "strategy", "dryRun")) elif name == "gitea.pullrequest.merge": properties.update( { "number": {"type": "integer", "minimum": 1, "maximum": 2_147_483_647}, "expectedHeadSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, "method": { "type": "string", "enum": ["merge", "rebase", "rebase-merge", "squash", "fast-forward-only"], "default": "squash", }, } ) required.extend(("number", "expectedHeadSha")) elif name == "gitea.branch.delete": properties.update( { "branch": change_branch, "expectedHeadSha": {"type": "string", "pattern": "^[0-9a-fA-F]{40,64}$"}, } ) required.extend(("branch", "expectedHeadSha")) elif name == "gitea.changeset.apply": return _changeset_schema(include_message=True) elif name == "gitea.validation.run": properties.update( { "branch": change_branch, "profile": { "type": "string", "enum": ["test", "lint", "typecheck", "build", "security", "full"], }, } ) required.extend(("branch", "profile")) elif name == "gitea.workspace.run": properties.update( { "branch": change_branch, "profile": {"type": "string", "enum": list(_WORKSPACE_PROFILES)}, } ) required.extend(("branch", "profile")) elif name == "gitea.orchestration.start": properties = { "repositoryRefs": { "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": True, "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$", }, }, "branchSuffix": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,79}$", }, "expectedOrchestrationId": {"type": "string", "pattern": "^[0-9a-f]{24}$"}, } required = ["repositoryRefs", "branchSuffix", "expectedOrchestrationId"] else: raise ValueError(f"unsupported Gitea write tool: {name}") return { "type": "object", "additionalProperties": False, "properties": properties, "required": required, } GITEA_PR_WRITE_TOOLS = tuple( ToolPublication( canonical_name=name, version=( "1.15.0" if name == "gitea.pullrequest.validation.ensure" else "1.14.0" if name == "gitea.pullrequest.close" else "1.13.0" if name in {"gitea.validation.run", "gitea.workspace.run"} else "1.10.0" if name == "gitea.issue.update" else "1.9.0" if name in { "gitea.branch.create", "gitea.file.create", "gitea.file.update", "gitea.file.delete", "gitea.pullrequest.create", "gitea.pullrequest.update", "gitea.changeset.apply", } else "1.9.0" if name in {"gitea.branchprotection.ensure", "gitea.portfolio.governance.apply"} else "1.8.0" if name == "gitea.repository.create" else "1.5.0" ), connector_type="gitea", read_only=False, risk_class="R2", input_schema=_write_tool_input_schema(name), output_schema=_PAGE_OUTPUT, required_scopes=("project:{project}:write",), ) for name in ( "gitea.repository.create", "gitea.branchprotection.ensure", "gitea.portfolio.governance.apply", "gitea.branch.create", "gitea.issue.create", "gitea.issue.update", "gitea.issue.comment.create", "gitea.file.create", "gitea.file.update", "gitea.file.delete", "gitea.pullrequest.create", "gitea.pullrequest.update", "gitea.pullrequest.close", "gitea.pullrequest.validation.ensure", "gitea.changeset.apply", "gitea.validation.run", "gitea.workspace.run", "gitea.orchestration.start", ) ) GITEA_LIFECYCLE_WRITE_TOOLS = tuple( ToolPublication( canonical_name=name, version=( "1.14.0" if name == "gitea.branch.delete" else "1.12.0" if name == "gitea.pullrequest.sync" else "1.10.0" ), connector_type="gitea", read_only=False, risk_class="R3", input_schema=_write_tool_input_schema(name), output_schema=_PAGE_OUTPUT, required_scopes=("project:{project}:write",), ) for name in ("gitea.pullrequest.sync", "gitea.pullrequest.merge", "gitea.branch.delete") ) GITEA_TOOLS = GITEA_READ_TOOLS GITEA_RESOURCES = ( ResourcePublication( uri_template="itworx-git://{project}/{repositoryRef}/{path}", connector_type="gitea", risk_class="R1", mime_types=("text/plain", "text/markdown", "application/json"), max_bytes=262144, required_scopes=("project:{project}:read",), ), )