Add GitHub integration service and endpoints

This commit is contained in:
2025-10-22 14:11:00 -06:00
parent d9f2a37b74
commit fcaf072d44
18 changed files with 725 additions and 3 deletions

View File

@@ -19,6 +19,7 @@ from .enrollment import (
PollingResult,
)
from .jobs.scheduler_service import SchedulerService
from .github import GitHubService, GitHubTokenPayload
from .realtime import AgentRealtimeService, AgentRecord
__all__ = [
@@ -37,4 +38,6 @@ __all__ = [
"AgentRealtimeService",
"AgentRecord",
"SchedulerService",
"GitHubService",
"GitHubTokenPayload",
]

View File

@@ -9,10 +9,12 @@ from pathlib import Path
from typing import Callable, Optional
from Data.Engine.config import EngineSettings
from Data.Engine.integrations.github import GitHubArtifactProvider
from Data.Engine.repositories.sqlite import (
SQLiteConnectionFactory,
SQLiteDeviceRepository,
SQLiteEnrollmentRepository,
SQLiteGitHubRepository,
SQLiteJobRepository,
SQLiteRefreshTokenRepository,
)
@@ -26,6 +28,7 @@ from Data.Engine.services.auth import (
from Data.Engine.services.crypto.signing import ScriptSigner, load_signer
from Data.Engine.services.enrollment import EnrollmentService
from Data.Engine.services.enrollment.nonce_cache import NonceCache
from Data.Engine.services.github import GitHubService
from Data.Engine.services.jobs import SchedulerService
from Data.Engine.services.rate_limit import SlidingWindowRateLimiter
from Data.Engine.services.realtime import AgentRealtimeService
@@ -42,6 +45,7 @@ class EngineServiceContainer:
dpop_validator: DPoPValidator
agent_realtime: AgentRealtimeService
scheduler_service: SchedulerService
github_service: GitHubService
def build_service_container(
@@ -56,6 +60,7 @@ def build_service_container(
token_repo = SQLiteRefreshTokenRepository(db_factory, logger=log.getChild("tokens"))
enrollment_repo = SQLiteEnrollmentRepository(db_factory, logger=log.getChild("enrollment"))
job_repo = SQLiteJobRepository(db_factory, logger=log.getChild("jobs"))
github_repo = SQLiteGitHubRepository(db_factory, logger=log.getChild("github_repo"))
jwt_service = load_jwt_service()
dpop_validator = DPoPValidator()
@@ -101,6 +106,20 @@ def build_service_container(
logger=log.getChild("scheduler"),
)
github_provider = GitHubArtifactProvider(
cache_file=settings.github.cache_file,
default_repo=settings.github.default_repo,
default_branch=settings.github.default_branch,
refresh_interval=settings.github.refresh_interval_seconds,
logger=log.getChild("github.provider"),
)
github_service = GitHubService(
repository=github_repo,
provider=github_provider,
logger=log.getChild("github"),
)
github_service.start_background_refresh()
return EngineServiceContainer(
device_auth=device_auth,
token_service=token_service,
@@ -109,6 +128,7 @@ def build_service_container(
dpop_validator=dpop_validator,
agent_realtime=agent_realtime,
scheduler_service=scheduler_service,
github_service=github_service,
)

View File

@@ -0,0 +1,8 @@
"""GitHub-oriented services for the Borealis Engine."""
from __future__ import annotations
from .github_service import GitHubService, GitHubTokenPayload
__all__ = ["GitHubService", "GitHubTokenPayload"]

View File

@@ -0,0 +1,106 @@
"""GitHub service layer bridging repositories and integrations."""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
from typing import Callable, Optional
from Data.Engine.domain.github import GitHubRepoRef, GitHubTokenStatus, RepoHeadSnapshot
from Data.Engine.integrations.github import GitHubArtifactProvider
from Data.Engine.repositories.sqlite.github_repository import SQLiteGitHubRepository
__all__ = ["GitHubService", "GitHubTokenPayload"]
@dataclass(frozen=True, slots=True)
class GitHubTokenPayload:
token: Optional[str]
status: GitHubTokenStatus
checked_at: int
def to_dict(self) -> dict:
payload = self.status.to_dict()
payload.update(
{
"token": self.token or "",
"checked_at": self.checked_at,
}
)
return payload
class GitHubService:
"""Coordinate GitHub caching, verification, and persistence."""
def __init__(
self,
*,
repository: SQLiteGitHubRepository,
provider: GitHubArtifactProvider,
logger: Optional[logging.Logger] = None,
clock: Optional[Callable[[], float]] = None,
) -> None:
self._repository = repository
self._provider = provider
self._log = logger or logging.getLogger("borealis.engine.services.github")
self._clock = clock or time.time
self._token_cache: Optional[str] = None
self._token_loaded_at: float = 0.0
initial_token = self._repository.load_token()
self._apply_token(initial_token)
def get_repo_head(
self,
owner_repo: Optional[str],
branch: Optional[str],
*,
ttl_seconds: int,
force_refresh: bool = False,
) -> RepoHeadSnapshot:
repo_str = (owner_repo or self._provider.default_repo).strip()
branch_name = (branch or self._provider.default_branch).strip()
repo = GitHubRepoRef.parse(repo_str, branch_name)
ttl = max(30, min(ttl_seconds, 3600))
return self._provider.fetch_repo_head(repo, ttl_seconds=ttl, force_refresh=force_refresh)
def refresh_default_repo(self, *, force: bool = False) -> RepoHeadSnapshot:
return self._provider.refresh_default_repo_head(force=force)
def get_token_status(self, *, force_refresh: bool = False) -> GitHubTokenPayload:
token = self._load_token(force_refresh=force_refresh)
status = self._provider.verify_token(token)
return GitHubTokenPayload(token=token, status=status, checked_at=int(self._clock()))
def update_token(self, token: Optional[str]) -> GitHubTokenPayload:
normalized = (token or "").strip()
self._repository.store_token(normalized)
self._apply_token(normalized)
status = self._provider.verify_token(normalized)
self._provider.start_background_refresh()
self._log.info("github-token updated valid=%s", status.valid)
return GitHubTokenPayload(token=normalized or None, status=status, checked_at=int(self._clock()))
def start_background_refresh(self) -> None:
self._provider.start_background_refresh()
@property
def default_refresh_interval(self) -> int:
return self._provider.refresh_interval
def _load_token(self, *, force_refresh: bool = False) -> Optional[str]:
now = self._clock()
if not force_refresh and self._token_cache is not None and (now - self._token_loaded_at) < 15.0:
return self._token_cache
token = self._repository.load_token()
self._apply_token(token)
return token
def _apply_token(self, token: Optional[str]) -> None:
self._token_cache = (token or "").strip() or None
self._token_loaded_at = self._clock()
self._provider.set_token(self._token_cache)