Make Gitea publishing claimant controlled

This commit is contained in:
2026-08-27 16:13:29 -07:00
parent 41e08611c9
commit a76ad5258b
10 changed files with 797 additions and 215 deletions
+94 -59
View File
@@ -50,7 +50,7 @@ class GiteaAdapter(BaseServiceAdapter):
def _get_headers(self, token: Optional[str] = None) -> Dict[str, str]:
t = token or self.get_effective_token()
headers = {
"User-Agent": "ThinkStorm-Orchestrator/0.1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Content-Type": "application/json"
}
if t:
@@ -62,13 +62,14 @@ class GiteaAdapter(BaseServiceAdapter):
loop = asyncio.get_running_loop()
try:
url = f"{self.endpoint}/api/v1/version"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
def fetch():
return requests.get(url, headers={"User-Agent": "ThinkStorm-Orchestrator/0.1"}, timeout=6.0)
return requests.get(url, headers=headers, timeout=15.0)
resp = await loop.run_in_executor(None, fetch)
elapsed = int((time.time() - start) * 1000)
if resp.status_code == 200:
data = resp.json()
version = data.get("version", "unknown")
elapsed = int((time.time() - start) * 1000)
return ServiceHealth(
service_id=self.service_id,
healthy=True,
@@ -77,15 +78,13 @@ class GiteaAdapter(BaseServiceAdapter):
response_time_ms=elapsed,
extra={"version": version}
)
else:
elapsed = int((time.time() - start) * 1000)
return ServiceHealth(
service_id=self.service_id,
healthy=False,
endpoint=self.endpoint,
message=f"Gitea HTTP error: {resp.status_code}",
response_time_ms=elapsed
)
return ServiceHealth(
service_id=self.service_id,
healthy=False,
endpoint=self.endpoint,
message=f"Gitea returned HTTP {resp.status_code}",
response_time_ms=elapsed
)
except Exception as e:
elapsed = int((time.time() - start) * 1000)
return ServiceHealth(
@@ -100,7 +99,7 @@ class GiteaAdapter(BaseServiceAdapter):
"""Ensures the 'thinkstorm' organization exists."""
headers = self._get_headers(token)
try:
r = session.get(f"{self.endpoint}/api/v1/orgs/{self.org_name}", headers=headers, timeout=8)
r = session.get(f"{self.endpoint}/api/v1/orgs/{self.org_name}", headers=headers, timeout=60)
if r.status_code == 200:
return
if r.status_code == 404:
@@ -110,7 +109,7 @@ class GiteaAdapter(BaseServiceAdapter):
"description": "Canonical dossiers, research, and project incubations generated by ThinkStorm",
"visibility": "public"
}
session.post(f"{self.endpoint}/api/v1/orgs", json=payload, headers=headers, timeout=8)
session.post(f"{self.endpoint}/api/v1/orgs", json=payload, headers=headers, timeout=60)
except Exception as e:
print(f"[Gitea] Org ensure notice: {e}")
@@ -118,7 +117,7 @@ class GiteaAdapter(BaseServiceAdapter):
"""Ensures the repository exists under the organization."""
headers = self._get_headers(token)
try:
r = session.get(f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}", headers=headers, timeout=8)
r = session.get(f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}", headers=headers, timeout=60)
if r.status_code == 200:
return True
if r.status_code == 404:
@@ -129,13 +128,38 @@ class GiteaAdapter(BaseServiceAdapter):
"auto_init": True,
"default_branch": "main"
}
cr = session.post(f"{self.endpoint}/api/v1/orgs/{self.org_name}/repos", json=payload, headers=headers, timeout=10)
cr = session.post(f"{self.endpoint}/api/v1/orgs/{self.org_name}/repos", json=payload, headers=headers, timeout=60)
return cr.status_code in (200, 201)
except Exception as e:
print(f"[Gitea] Repo ensure error for {repo_name}: {e}")
return False
return False
def _grant_repo_write_access(
self,
session: requests.Session,
token: str,
repo_name: str,
username: str
) -> None:
"""Idempotently grants the verified claimant write access to a repository."""
safe_username = requests.utils.quote(username, safe="")
url = (
f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}"
f"/collaborators/{safe_username}"
)
response = session.put(
url,
json={"permission": "write"},
headers=self._get_headers(token),
timeout=60
)
if response.status_code not in (200, 201, 204):
raise RuntimeError(
f"Could not grant Gitea write access to '{username}' "
f"(HTTP {response.status_code})."
)
def _sync_files_via_api(self, token: str, repo_slug: str, files_dict: Dict[str, str]) -> None:
"""Commits or updates multiple files directly into the Gitea repository via Contents API."""
if not files_dict or not token:
@@ -147,7 +171,7 @@ class GiteaAdapter(BaseServiceAdapter):
try:
# 1. Check if file already exists in repo to get current SHA
get_url = f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_slug}/contents/{path}"
r = s.get(get_url, headers=headers, timeout=8)
r = s.get(get_url, headers=headers, timeout=60)
sha = None
if r.status_code == 200:
sha = r.json().get("sha")
@@ -161,11 +185,11 @@ class GiteaAdapter(BaseServiceAdapter):
if sha:
payload["sha"] = sha
put_res = s.put(get_url, json=payload, headers=headers, timeout=10)
put_res = s.put(get_url, json=payload, headers=headers, timeout=60)
if put_res.status_code not in (200, 201):
print(f"[Gitea] Update notice for {path}: {put_res.status_code} {put_res.text[:100]}")
else:
post_res = s.post(get_url, json=payload, headers=headers, timeout=10)
post_res = s.post(get_url, json=payload, headers=headers, timeout=60)
if post_res.status_code not in (200, 201):
print(f"[Gitea] Create notice for {path}: {post_res.status_code} {post_res.text[:100]}")
except Exception as e:
@@ -184,9 +208,11 @@ class GiteaAdapter(BaseServiceAdapter):
outputs: Dict[str, str],
provenance_runs: List[Dict[str, Any]],
existing_repo_url: Optional[str] = None,
submission_image: Optional[Dict[str, Any]] = None
submission_image: Optional[Dict[str, Any]] = None,
publish_remote: bool = True,
collaborator_username: Optional[str] = None
) -> Dict[str, Any]:
"""Creates a dedicated Gitea Project repository and synchronizes all files into it."""
"""Persists a dossier locally and optionally publishes it to Gitea."""
# 1. Local disk directory
idea_dir = ARTIFACTS_DIR / idea_id
idea_dir.mkdir(parents=True, exist_ok=True)
@@ -284,43 +310,43 @@ class GiteaAdapter(BaseServiceAdapter):
clone_url = f"{self.endpoint}/{self.org_name}/{repo_name}.git"
ssh_url = f"ssh://[email protected]:22/{self.org_name}/{repo_name}.git"
token = self.get_effective_token()
if token:
token = self.get_effective_token() if publish_remote else ""
if publish_remote:
if not token:
raise RuntimeError("Gitea publishing is not configured with a service token.")
loop = asyncio.get_running_loop()
def sync_gitea():
try:
with requests.Session() as s:
# 1. Ensure org and repo exist
self._ensure_org(s, token)
self._ensure_repo(s, token, repo_name, f"[{idea_id}] {title}")
# 2. Push full file tree to Gitea
self._sync_files_via_api(token, repo_name, files_to_sync)
return f"{self.endpoint}/{self.org_name}/{repo_name}"
except Exception as e:
print(f"[Gitea] Sync error: {e}")
return repo_url
with requests.Session() as s:
self._ensure_org(s, token)
if not self._ensure_repo(s, token, repo_name, f"[{idea_id}] {title}"):
raise RuntimeError(f"Could not create or access Gitea repository '{repo_name}'.")
if collaborator_username:
self._grant_repo_write_access(
s, token, repo_name, collaborator_username
)
self._sync_files_via_api(token, repo_name, files_to_sync)
return f"{self.endpoint}/{self.org_name}/{repo_name}"
try:
actual_url = await loop.run_in_executor(None, sync_gitea)
repo_url = actual_url or repo_url
except Exception as e:
print(f"[Gitea] Async sync exception: {e}")
actual_url = await loop.run_in_executor(None, sync_gitea)
repo_url = actual_url or repo_url
return {
"repo_name": f"{self.org_name}/{repo_name}",
"repo_url": repo_url,
"clone_url": clone_url,
"ssh_url": ssh_url,
"local_path": str(idea_dir)
"local_path": str(idea_dir),
"published": publish_remote
}
async def persist_work_track_outputs(
self,
idea_id: str,
track_name: str,
outputs: Dict[str, str]
outputs: Dict[str, str],
publish_remote: bool = True
) -> Dict[str, Any]:
"""Persists Work Track deliverables to local dossier and commits to Gitea project."""
"""Persists Work Track deliverables locally and optionally pushes them."""
idea_dir = ARTIFACTS_DIR / idea_id
out_subdir = idea_dir / "outputs" / track_name.lower().replace(" ", "-")
out_subdir.mkdir(parents=True, exist_ok=True)
@@ -335,7 +361,7 @@ class GiteaAdapter(BaseServiceAdapter):
repo_name = f"ts-{clean_slug.replace('ts-', '')}"
repo_url = f"{self.endpoint}/{self.org_name}/{repo_name}"
token = self.get_effective_token()
token = self.get_effective_token() if publish_remote else ""
if token and files_to_sync:
loop = asyncio.get_running_loop()
try:
@@ -348,31 +374,40 @@ class GiteaAdapter(BaseServiceAdapter):
print(f"[Gitea] Work track sync notice: {e}")
return {
"status": "SUCCESS",
"status": "SUCCESS" if publish_remote else "LOCAL_ONLY",
"path": str(out_subdir),
"repo_url": repo_url
"repo_url": repo_url if publish_remote else ""
}
async def graduate_project(self, idea_id: str, title: str, summary: str, repo_name: str = "") -> Dict[str, Any]:
async def graduate_project(
self,
idea_id: str,
title: str,
summary: str,
repo_name: str = "",
collaborator_username: Optional[str] = None
) -> Dict[str, Any]:
"""Graduates an incubated Coding Project work track to a dedicated repository under 'thinkstorm'."""
clean_title = re.sub(r'[^a-zA-Z0-9_-]', '-', title.lower()).strip('-')[:30] or "project"
slug = repo_name or f"ts-{idea_id.lower()}-{clean_title}"
repo_url = f"{self.endpoint}/{self.org_name}/{slug}"
token = self.get_effective_token()
if token:
loop = asyncio.get_running_loop()
def create_remote():
try:
with requests.Session() as s:
self._ensure_org(s, token)
self._ensure_repo(s, token, slug, f"[{idea_id}] {title}")
return f"{self.endpoint}/{self.org_name}/{slug}"
except Exception as e:
print(f"[Gitea] Project graduation notice: {e}")
return repo_url
actual_url = await loop.run_in_executor(None, create_remote)
repo_url = actual_url or repo_url
if not token:
raise RuntimeError("Gitea publishing is not configured with a service token.")
loop = asyncio.get_running_loop()
def create_remote():
with requests.Session() as s:
self._ensure_org(s, token)
if not self._ensure_repo(s, token, slug, f"[{idea_id}] {title}"):
raise RuntimeError(f"Could not create or access Gitea repository '{slug}'.")
if collaborator_username:
self._grant_repo_write_access(
s, token, slug, collaborator_username
)
return f"{self.endpoint}/{self.org_name}/{slug}"
actual_url = await loop.run_in_executor(None, create_remote)
repo_url = actual_url or repo_url
return {
"repo_name": f"{self.org_name}/{slug}",