From 7b31e68a7b05f005dedcf912d8c525817d90e1dd Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 19:09:27 +0800 Subject: [PATCH] feat(api): validate gitea private inventory payloads --- apps/api/src/api/v1/agents.py | 39 +++ ...henticated_inventory_payload_validation.py | 328 ++++++++++++++++++ .../gitea_private_inventory_p0_scorecard.py | 11 + ...itea_private_inventory_p0_scorecard_api.py | 158 +++++++++ 4 files changed, 536 insertions(+) create mode 100644 apps/api/src/services/gitea_authenticated_inventory_payload_validation.py diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 3a4519167..d74669623 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -341,6 +341,9 @@ from src.services.docker_build_surface_inventory import ( from src.services.gitea_private_inventory_p0_scorecard import ( load_latest_gitea_private_inventory_p0_scorecard, ) +from src.services.gitea_authenticated_inventory_payload_validation import ( + validate_gitea_authenticated_inventory_payload, +) from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) @@ -1073,6 +1076,42 @@ async def get_gitea_private_inventory_p0_scorecard() -> dict[str, Any]: ) from exc +@router.post( + "/gitea-private-inventory-p0-scorecard/validate-authenticated-inventory-payload", + response_model=dict[str, Any], + summary="驗證 P0-003 Gitea authenticated/admin inventory 脫敏 payload", + description=( + "針對單次 owner-provided redacted Gitea authenticated/admin inventory " + "payload 進行 no-persist validation;此端點只回傳 private inventory " + "reviewer readiness / needs supplement / quarantined / rejected execution " + "分流,不保存 payload、不呼叫 Gitea/GitHub、不收 token value、" + "不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、" + "不讀 secret、不讀 raw session/SQLite。" + ), +) +async def validate_gitea_authenticated_inventory_payload_packet( + inventory_payload: dict[str, Any], +) -> dict[str, Any]: + """Return no-persist validation for one P0-003 redacted inventory payload.""" + try: + payload = await asyncio.to_thread( + validate_gitea_authenticated_inventory_payload, + inventory_payload, + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("gitea_authenticated_inventory_payload_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P0-003 Gitea authenticated inventory payload 驗證器無效", + ) from exc + + @router.get( "/github-target-private-backup-evidence-gate", response_model=dict[str, Any], diff --git a/apps/api/src/services/gitea_authenticated_inventory_payload_validation.py b/apps/api/src/services/gitea_authenticated_inventory_payload_validation.py new file mode 100644 index 000000000..f03efda55 --- /dev/null +++ b/apps/api/src/services/gitea_authenticated_inventory_payload_validation.py @@ -0,0 +1,328 @@ +"""P0-003 Gitea authenticated/admin inventory payload validation. + +This service validates one redacted Gitea inventory payload for reviewer +readiness. It never calls Gitea or GitHub, never stores token values, and never +writes repos, refs, secrets, workflow state, or runtime state. +""" + +from __future__ import annotations + +import re +from typing import Any +from urllib.parse import parse_qsl, urlsplit + +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) + +_SCHEMA_VERSION = "gitea_authenticated_inventory_payload_validation_v1" +_PAYLOAD_SCHEMA_VERSION = "gitea_repo_inventory_v1" +_ACCEPTED_VISIBILITY_SCOPES = {"authenticated", "admin_export"} +_REQUIRED_ATTESTATIONS = { + "no_token_value", + "no_write_token", + "no_webhook_secret", + "no_deploy_key_private_key", + "no_runner_registration_token", + "no_cookie_or_session", + "no_gitea_db_dump", + "no_git_object_pack", +} +_FORBIDDEN_TRUE_FIELDS = { + "repo_write_allowed", + "refs_sync_allowed", + "github_primary_switch_authorized", + "runtime_execution_authorized", + "write_to_gitea", + "create_gitea_repo", + "delete_or_archive_gitea_repo", + "sync_git_refs", + "force_push", +} +_SECRET_PATTERNS = { + "authorization_header": re.compile(r"Authorization\s*:", re.IGNORECASE), + "bearer_token": re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{12,}", re.IGNORECASE), + "cookie_header": re.compile(r"\bCookie\s*:", re.IGNORECASE), + "password_assignment": re.compile(r"\bpassword\s*[:=]\s*[^,\s]+", re.IGNORECASE), + "private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), + "token_assignment": re.compile(r"\btoken\s*[:=]\s*[^,\s]+", re.IGNORECASE), +} +_SECRET_QUERY_KEYS = {"access_token", "auth", "key", "password", "secret", "token"} +_BLOCKERS_CLEARED_BY_ACCEPTED_PAYLOAD = { + "gitea_repo_inventory_status_not_ok", + "gitea_visibility_scope_public_only_or_unknown", + "gitea_authenticated_inventory_payload_not_accepted", +} + + +def validate_gitea_authenticated_inventory_payload( + inventory_payload: dict[str, Any], + scorecard: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Validate one redacted P0-003 inventory payload without persisting it.""" + current = scorecard or load_latest_gitea_private_inventory_p0_scorecard() + blockers: list[str] = [] + sensitive_hits = _find_sensitive_strings(inventory_payload) + forbidden_true_fields = _find_forbidden_true_fields(inventory_payload) + + if inventory_payload.get("schema_version") != _PAYLOAD_SCHEMA_VERSION: + blockers.append(f"schema_version_not_{_PAYLOAD_SCHEMA_VERSION}") + if inventory_payload.get("status") != "ok": + blockers.append("status_not_ok") + visibility_scope = str(inventory_payload.get("visibility_scope") or "") + if visibility_scope not in _ACCEPTED_VISIBILITY_SCOPES: + blockers.append("visibility_scope_not_authenticated_or_admin_export") + + repos = [ + repo for repo in _as_list(inventory_payload.get("repos")) if isinstance(repo, dict) + ] + repo_count = _as_int(inventory_payload.get("repo_count")) + if repo_count != len(repos): + blockers.append("repo_count_mismatch") + if repo_count < 4: + blockers.append("repo_count_below_current_public_floor") + blockers.extend(_validate_repos(repos)) + + if _is_placeholder(inventory_payload.get("coverage_gap_explanation")): + blockers.append("coverage_gap_explanation_missing") + blockers.extend( + _validate_redaction_attestation(inventory_payload.get("redaction_attestation")) + ) + + if forbidden_true_fields: + status = "rejected_execution_request" + elif sensitive_hits: + status = "quarantined_sensitive_payload" + elif blockers: + status = "needs_supplement" + else: + status = "accepted_for_private_inventory_review_only" + + accepted = status == "accepted_for_private_inventory_review_only" + current_rollups = _as_dict(current.get("rollups")) + current_blockers = _strings(current.get("active_blockers")) + projected_blockers = ( + [ + blocker + for blocker in current_blockers + if blocker not in _BLOCKERS_CLEARED_BY_ACCEPTED_PAYLOAD + ] + if accepted + else current_blockers + ) + current_accepted_payload_count = _as_int( + current_rollups.get("accepted_inventory_payload_count") + ) + projected_accepted_payload_count = ( + max(current_accepted_payload_count, 1) if accepted else current_accepted_payload_count + ) + owner_attestation_accepted_count = _as_int( + current_rollups.get("owner_coverage_attestation_accepted_count") + ) + + return { + "schema_version": _SCHEMA_VERSION, + "status": status, + "priority": "P0-003", + "scope": "gitea_authenticated_inventory_payload_validation", + "source_scorecard_status": current.get("status"), + "result": { + "accepted_payload_count": 1 if accepted else 0, + "repo_count": repo_count, + "visible_repo_count": len(repos), + "blocker_count": len(blockers), + "sensitive_payload_hit_count": len(sensitive_hits), + "forbidden_true_field_count": len(forbidden_true_fields), + "current_active_blocker_count": len(current_blockers), + "projected_active_blocker_count": len(projected_blockers), + "current_accepted_inventory_payload_count": current_accepted_payload_count, + "projected_accepted_inventory_payload_count": projected_accepted_payload_count, + "projected_gitea_repo_inventory_status": ( + "ok" if accepted else current_rollups.get("gitea_repo_inventory_status") + ), + "projected_gitea_visibility_scope": ( + visibility_scope + if accepted + else current_rollups.get("gitea_visibility_scope") + ), + "owner_coverage_attestation_still_required_count": ( + 0 if owner_attestation_accepted_count > 0 else 1 + ), + "token_value_collection_allowed": False, + "repo_write_allowed": False, + "refs_sync_allowed": False, + "github_primary_switch_authorized": False, + "runtime_gate_count": 0, + }, + "blockers": blockers, + "sensitive_payload_hits": sensitive_hits, + "forbidden_true_fields": forbidden_true_fields, + "operation_boundaries": { + "payload_persisted": False, + "gitea_api_called": False, + "gitea_write_performed": False, + "repo_write_performed": False, + "refs_sync_performed": False, + "github_api_used": False, + "github_cli_used": False, + "secret_plaintext_read": False, + "token_value_collection_allowed": False, + "runtime_action_performed": False, + "raw_session_or_sqlite_read_performed": False, + }, + "reviewer_readiness": { + "schema_version": "gitea_private_inventory_reviewer_readiness_v1", + "status": ( + "ready_for_owner_coverage_attestation_review" + if accepted + else "not_ready_for_owner_coverage_attestation_review" + ), + "accepted_repo_count": repo_count if accepted else 0, + "projected_active_blocker_count": len(projected_blockers), + "projected_remaining_blockers": projected_blockers, + "redacted_inventory_receipt_writeback_ready_count": 1 if accepted else 0, + "owner_coverage_attestation_still_required_count": ( + 0 if owner_attestation_accepted_count > 0 else 1 + ), + "repo_write_authorized_count": 0, + "refs_sync_authorized_count": 0, + "github_primary_switch_authorized_count": 0, + "runtime_gate_count": 0, + "token_value_collection_allowed": False, + "payload_persisted": False, + "safe_next_step": ( + "write_redacted_inventory_review_receipt_then_collect_owner_coverage_attestation" + if accepted + else "supplement_authenticated_or_admin_export_redacted_inventory_payload" + ), + "blocked_operations": [ + "store_token_value", + "gitea_api_write", + "repo_write", + "refs_sync", + "github_api", + "github_primary_switch", + "workflow_dispatch", + "raw_session_or_sqlite_read", + ], + }, + "safe_next_step": ( + "review_redacted_inventory_payload_then_collect_owner_coverage_attestation" + if accepted + else "supplement_authenticated_or_admin_export_redacted_inventory_payload" + ), + } + + +def _validate_repos(repos: list[dict[str, Any]]) -> list[str]: + blockers: list[str] = [] + seen: set[str] = set() + for index, repo in enumerate(repos): + identity = str(repo.get("full_name") or repo.get("gitea_repo") or "") + if not identity: + blockers.append(f"repos[{index}].identity_missing") + elif identity in seen: + blockers.append(f"repos[{index}].identity_duplicate") + seen.add(identity) + for key in ("name", "default_branch", "clone_url_redacted", "ssh_url_redacted"): + if _is_placeholder(repo.get(key)): + blockers.append(f"repos[{index}].{key}_missing") + owner = repo.get("owner") + if _is_placeholder(owner) and _is_placeholder(_as_dict(owner).get("login")): + blockers.append(f"repos[{index}].owner_missing") + for key in ("private", "archived", "empty"): + if not isinstance(repo.get(key), bool): + blockers.append(f"repos[{index}].{key}_not_boolean") + for key in ("clone_url_redacted", "ssh_url_redacted"): + value = str(repo.get(key) or "") + if _url_has_secret(value): + blockers.append(f"repos[{index}].{key}_not_redacted") + return blockers + + +def _validate_redaction_attestation(value: Any) -> list[str]: + attestation = _as_dict(value) + if not attestation: + return ["redaction_attestation_missing"] + return [ + f"redaction_attestation.{key}_not_true" + for key in sorted(_REQUIRED_ATTESTATIONS) + if attestation.get(key) is not True + ] + + +def _find_sensitive_strings(value: Any) -> list[str]: + hits: list[str] = [] + + def walk(node: Any, path: str) -> None: + if isinstance(node, dict): + for key, item in node.items(): + walk(item, f"{path}.{key}" if path else str(key)) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]") + elif isinstance(node, str): + for name, pattern in _SECRET_PATTERNS.items(): + if pattern.search(node): + hits.append(f"{path}:{name}") + if _url_has_secret(node): + hits.append(f"{path}:url_contains_secret_material") + + walk(value, "") + return sorted(set(hits)) + + +def _find_forbidden_true_fields(value: Any) -> list[str]: + hits: list[str] = [] + + def walk(node: Any, path: str) -> None: + if isinstance(node, dict): + for key, item in node.items(): + next_path = f"{path}.{key}" if path else str(key) + if key in _FORBIDDEN_TRUE_FIELDS and item is True: + hits.append(next_path) + walk(item, next_path) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]") + + walk(value, "") + return sorted(hits) + + +def _url_has_secret(value: str) -> bool: + if "://" not in value: + return False + parsed = urlsplit(value) + if parsed.username or parsed.password: + return True + return any(key.lower() in _SECRET_QUERY_KEYS for key, _ in parse_qsl(parsed.query)) + + +def _is_placeholder(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str): + return value.strip().lower() in {"", "pending", "todo", "tbd", "n/a", "na"} + return False + + +def _strings(value: Any) -> list[str]: + if not isinstance(value, list): + return [] + return [str(item) for item in value if item is not None] + + +def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + +def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _as_int(value: Any) -> int: + try: + return int(value) + except (TypeError, ValueError): + return 0 diff --git a/apps/api/src/services/gitea_private_inventory_p0_scorecard.py b/apps/api/src/services/gitea_private_inventory_p0_scorecard.py index 2e46a535d..424f0a86d 100644 --- a/apps/api/src/services/gitea_private_inventory_p0_scorecard.py +++ b/apps/api/src/services/gitea_private_inventory_p0_scorecard.py @@ -40,6 +40,7 @@ def load_latest_gitea_private_inventory_p0_scorecard( def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: gitea_inventory = _dict(scorecard.get("gitea_inventory")) import_acceptance = _dict(scorecard.get("authenticated_import_acceptance")) + payload_validation = _dict(scorecard.get("authenticated_payload_validation")) coverage_attestation = _dict(scorecard.get("coverage_attestation")) owner_response_validation = _dict(scorecard.get("owner_response_validation")) product_coverage = _dict(scorecard.get("product_row_coverage")) @@ -70,6 +71,7 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: }, "gitea_inventory": gitea_inventory, "authenticated_import_acceptance": import_acceptance, + "authenticated_payload_validation": payload_validation, "coverage_attestation": coverage_attestation, "owner_response_validation": owner_response_validation, "product_row_coverage": product_coverage, @@ -89,6 +91,15 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "accepted_inventory_payload_count": _int( import_acceptance.get("accepted_payload_count") ), + "authenticated_payload_validation_status": str( + payload_validation.get("status") or "unknown" + ), + "authenticated_payload_validation_accepted_count": _int( + payload_validation.get("accepted_payload_count") + ), + "authenticated_payload_validation_blocker_count": _int( + payload_validation.get("blocker_count") + ), "owner_coverage_attestation_received_count": _int( coverage_attestation.get("received_attestation_count") ), diff --git a/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py index 32a32b905..96ef6ba47 100644 --- a/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py +++ b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py @@ -7,6 +7,9 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from src.api.v1.agents import router +from src.services.gitea_authenticated_inventory_payload_validation import ( + validate_gitea_authenticated_inventory_payload, +) from src.services.gitea_private_inventory_p0_scorecard import ( load_latest_gitea_private_inventory_p0_scorecard, ) @@ -25,6 +28,9 @@ def test_gitea_private_inventory_p0_scorecard_reports_active_gitea_blocker(): assert payload["rollups"]["gitea_repo_inventory_status"] == "partial" assert payload["rollups"]["gitea_visibility_scope"] == "public_only" assert payload["rollups"]["accepted_inventory_payload_count"] == 0 + assert payload["rollups"]["authenticated_payload_validation_status"] == "needs_supplement" + assert payload["rollups"]["authenticated_payload_validation_accepted_count"] == 0 + assert payload["rollups"]["authenticated_payload_validation_blocker_count"] == 4 assert payload["rollups"]["owner_coverage_attestation_received_count"] == 0 assert payload["rollups"]["owner_response_validation_received_count"] == 0 assert payload["rollups"]["owner_response_validation_accepted_count"] == 0 @@ -44,6 +50,7 @@ def test_gitea_private_inventory_p0_scorecard_reports_active_gitea_blocker(): assert payload["rollups"]["github_lane_excluded_from_p0_blocker_count"] is True assert payload["github_retired_context"]["status"] == "stopped_retired_do_not_use" assert payload["github_retired_context"]["excluded_from_active_p0_blocker_math"] is True + assert payload["authenticated_payload_validation"]["status"] == "needs_supplement" assert "gitea_authenticated_inventory_payload_not_accepted" in payload["active_blockers"] @@ -74,6 +81,89 @@ def test_gitea_private_inventory_p0_scorecard_endpoint_returns_readback(): assert data["operation_boundaries"]["github_api_allowed"] is False assert data["operation_boundaries"]["github_cli_allowed"] is False assert data["operation_boundaries"]["raw_session_or_sqlite_read_allowed"] is False + assert data["authenticated_payload_validation"]["accepted_payload_count"] == 0 + + +def test_gitea_authenticated_inventory_payload_validator_accepts_redacted_admin_export(): + payload = validate_gitea_authenticated_inventory_payload( + _valid_authenticated_inventory_payload(), + scorecard=_scorecard_readback(), + ) + + assert payload["schema_version"] == "gitea_authenticated_inventory_payload_validation_v1" + assert payload["status"] == "accepted_for_private_inventory_review_only" + assert payload["result"]["accepted_payload_count"] == 1 + assert payload["result"]["repo_count"] == 4 + assert payload["result"]["current_active_blocker_count"] == 4 + assert payload["result"]["projected_active_blocker_count"] == 1 + assert payload["result"]["projected_accepted_inventory_payload_count"] == 1 + assert payload["result"]["projected_gitea_repo_inventory_status"] == "ok" + assert payload["result"]["projected_gitea_visibility_scope"] == "admin_export" + assert payload["result"]["runtime_gate_count"] == 0 + assert payload["operation_boundaries"]["payload_persisted"] is False + assert payload["operation_boundaries"]["gitea_api_called"] is False + assert payload["operation_boundaries"]["repo_write_performed"] is False + assert payload["operation_boundaries"]["refs_sync_performed"] is False + assert payload["operation_boundaries"]["github_api_used"] is False + assert payload["operation_boundaries"]["secret_plaintext_read"] is False + assert ( + payload["reviewer_readiness"]["status"] + == "ready_for_owner_coverage_attestation_review" + ) + assert payload["reviewer_readiness"]["redacted_inventory_receipt_writeback_ready_count"] == 1 + assert payload["reviewer_readiness"]["owner_coverage_attestation_still_required_count"] == 1 + assert payload["reviewer_readiness"]["repo_write_authorized_count"] == 0 + + +def test_gitea_authenticated_inventory_payload_endpoint_validates_without_persisting(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.post( + "/api/v1/agents/gitea-private-inventory-p0-scorecard/validate-authenticated-inventory-payload", + json=_valid_authenticated_inventory_payload(), + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "accepted_for_private_inventory_review_only" + assert data["result"]["accepted_payload_count"] == 1 + assert data["result"]["projected_active_blocker_count"] == 1 + assert data["reviewer_readiness"]["redacted_inventory_receipt_writeback_ready_count"] == 1 + assert data["operation_boundaries"]["payload_persisted"] is False + assert data["operation_boundaries"]["gitea_write_performed"] is False + assert data["operation_boundaries"]["raw_session_or_sqlite_read_performed"] is False + + +def test_gitea_authenticated_inventory_payload_validator_quarantines_secret_material(): + payload = _valid_authenticated_inventory_payload() + payload["repos"][0]["clone_url_redacted"] = "https://user:password@example.test/repo.git" + + validation = validate_gitea_authenticated_inventory_payload( + payload, + scorecard=_scorecard_readback(), + ) + + assert validation["status"] == "quarantined_sensitive_payload" + assert validation["result"]["accepted_payload_count"] == 0 + assert validation["result"]["sensitive_payload_hit_count"] >= 1 + assert validation["operation_boundaries"]["payload_persisted"] is False + + +def test_gitea_authenticated_inventory_payload_validator_rejects_execution_request(): + payload = _valid_authenticated_inventory_payload() + payload["repo_write_allowed"] = True + + validation = validate_gitea_authenticated_inventory_payload( + payload, + scorecard=_scorecard_readback(), + ) + + assert validation["status"] == "rejected_execution_request" + assert validation["result"]["accepted_payload_count"] == 0 + assert validation["result"]["forbidden_true_field_count"] == 1 + assert validation["operation_boundaries"]["repo_write_performed"] is False def test_gitea_private_inventory_p0_scorecard_rejects_github_reactivation(tmp_path): @@ -208,3 +298,71 @@ def _scorecard_payload() -> dict: "then_validate_import_acceptance_and_owner_attestation" ), } + + +def _scorecard_readback() -> dict: + return { + "status": "blocked_waiting_gitea_authenticated_or_owner_export_inventory", + "active_blockers": [ + "gitea_repo_inventory_status_not_ok", + "gitea_visibility_scope_public_only_or_unknown", + "gitea_authenticated_inventory_payload_not_accepted", + "gitea_owner_coverage_attestation_not_received", + ], + "rollups": { + "accepted_inventory_payload_count": 0, + "owner_coverage_attestation_accepted_count": 0, + "gitea_repo_inventory_status": "partial", + "gitea_visibility_scope": "public_only", + }, + } + + +def _valid_authenticated_inventory_payload() -> dict: + repos = [ + _repo("wooo/awoooi"), + _repo("wooo/ewoooc"), + _repo("wooo/agent-bounty-protocol"), + _repo("wooo/2026FIFAWorldCup"), + ] + return { + "schema_version": "gitea_repo_inventory_v1", + "base_url": "https://gitea.wooo.work", + "org": "wooo", + "visibility_scope": "admin_export", + "token_present": False, + "status": "ok", + "repo_count": len(repos), + "repos": repos, + "coverage_gap_explanation": { + "public_only_vs_admin_export": "admin export includes all in-scope repos", + "internal_110_adjacent_scope": "covered by owner scope decision", + "org_user_endpoint_identity": "wooo namespace owner confirmed", + }, + "redaction_attestation": { + "no_token_value": True, + "no_write_token": True, + "no_webhook_secret": True, + "no_deploy_key_private_key": True, + "no_runner_registration_token": True, + "no_cookie_or_session": True, + "no_gitea_db_dump": True, + "no_git_object_pack": True, + }, + } + + +def _repo(full_name: str) -> dict: + _, name = full_name.split("/", 1) + return { + "gitea_repo": full_name, + "name": name, + "owner": "wooo", + "private": False, + "empty": False, + "archived": False, + "default_branch": "main", + "clone_url_redacted": f"https://gitea.wooo.work/{full_name}.git", + "ssh_url_redacted": f"ssh://gitea.wooo.work/{full_name}.git", + "github_repo_candidate": "", + }