Files
ITWorx_MCP_Hub-Public/connectors/gitea/server.py
T

4902 lines
205 KiB
Python

"""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
from urllib.parse import quote, unquote, urlparse
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
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 _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")
if current is not None:
existing_contexts = current.get("status_check_contexts")
if isinstance(existing_contexts, list):
desired["status_check_contexts"] = sorted(
{
MANAGED_VALIDATION_CONTEXT,
*(str(item) for item in existing_contexts if str(item).strip()),
}
)
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_action = (
"create"
if workflow_content is None
else "none"
if workflow_digest == MANAGED_VALIDATION_DIGEST
else "update"
)
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": MANAGED_VALIDATION_DIGEST,
"action": workflow_action,
},
"branchProtection": {
"current": current_projection,
"desired": desired,
"diff": protection_diff,
"action": protection_action,
},
"blockers": blockers,
}
plan_hash = hashlib.sha256(
json.dumps(plan_payload, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
return {
**plan_payload,
"planHash": plan_hash,
"compliant": not blockers and workflow_action == "none" and protection_action == "none",
"_repository": repository,
"_workflowContent": workflow_content,
"_workflowSha": workflow_sha,
"_currentProtection": current,
}
@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
) -> dict[str, Any]:
plan = self._governance_plan_data(context, repository_ref)
if (
not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash)
or expected_plan_hash != plan["planHash"]
):
raise GiteaError(
"GITEA_PLAN_STALE",
"governance state changed; preview the repository again before applying",
)
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), "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":
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)
if not verified_plan["compliant"]:
raise GiteaError(
"GITEA_GOVERNANCE_VERIFY_FAILED",
"Gitea did not retain the requested governance contract",
)
return self._envelope(
context,
{
**self._public_governance_plan(verified_plan),
"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:
result = self.branch_protection_ensure(
context, repository_ref, str(item.get("planHash", ""))
)["data"]
items.append(
{
"repositoryRef": repository_ref,
"applied": True,
"changed": bool(result.get("changed")),
"verified": bool(result.get("verified")),
}
)
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",
}
else (
"1.8.0"
if name.startswith("gitea.branchprotection.")
or name.startswith("gitea.portfolio.governance.")
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}$"}
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}$"},
},
"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.8.0"
if name
in {
"gitea.repository.create",
"gitea.branchprotection.ensure",
"gitea.portfolio.governance.apply",
}
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",),
),
)