CIS490/receiver/config.py
max cc0c96953e version_gate: Forgejo as canonical commit source (no fs perms needed)
Initial git-log-based gate ran into a permission wall: the cis490
service user can't read /home/max/cis490/.git (ProtectHome=true +
home-dir mode). Switching the production source to the local Forgejo
HTTP API (already accessible to all WG peers, single source of truth
both lab hosts and the receiver pull from). When the maintainer
pushes new code to spectral/CIS490, the next 5-second cache refresh
sees the new commit and lab hosts can immediately ship under it.

VersionGate now takes either:
  - forgejo_url + repo_owner + repo_name + branch (+ optional
    auth_token for private repos): hits
    /api/v1/repos/<owner>/<name>/commits?sha=<branch>&limit=<n>
  - repo_path: dev-only fallback, runs `git log` locally

Local-git path retained for tests + the dev-only case.

receiver.toml.example gains forgejo_url/repo_owner/repo_name/branch
with auth_token commented; live-deployed receiver.toml on the Pi has
the spectral org + token.

Live state on the Pi: 41 valid hashes loaded, head=f8ad02b. Verified
end-to-end:
  bogus commit → 412 + remediation
  HEAD commit  → clears gate (fails downstream at sha-mismatch as
                 expected for the empty-body verify probe)

Test added: test_forgejo_backend_accepts_returned_commits stands up
a tiny canned-response HTTPServer in-process, exercises the parser
without depending on a live Forgejo instance. Brings test_version_gate
to 10 cases; total 158/158.
2026-05-01 01:42:45 -05:00

59 lines
2.2 KiB
Python

from __future__ import annotations
import tomllib
from dataclasses import dataclass
from pathlib import Path
DEFAULT_MAX_EPISODE_BYTES = 256 * 1024 * 1024
@dataclass(frozen=True)
class ReceiverConfig:
listen_host: str
listen_port: int
store_root: Path
incoming_root: Path
index_path: Path
max_episode_bytes: int
bearer_token: str | None
# Code-version gate. Production source is the local Forgejo
# (canonical repo both lab hosts and the receiver pull from);
# local-git path is a dev-only fallback.
version_gate_enabled: bool
version_gate_window: int
version_gate_forgejo_url: str | None
version_gate_repo_owner: str | None
version_gate_repo_name: str | None
version_gate_branch: str
version_gate_auth_token: str | None
version_gate_local_repo: Path | None
@classmethod
def load(cls, path: str | Path) -> "ReceiverConfig":
with open(path, "rb") as f:
data = tomllib.load(f)
listen_addr = data.get("listen_addr", "127.0.0.1:8443")
host, _, port = listen_addr.rpartition(":")
version_gate = data.get("version_gate", {})
local_repo = version_gate.get("local_repo_path")
return cls(
listen_host=host or "127.0.0.1",
listen_port=int(port),
store_root=Path(data["store_root"]).resolve(),
incoming_root=Path(data["incoming_root"]).resolve(),
index_path=Path(data["index_path"]).resolve(),
max_episode_bytes=int(
data.get("limits", {}).get("max_episode_bytes", DEFAULT_MAX_EPISODE_BYTES)
),
bearer_token=data.get("auth", {}).get("bearer_token"),
version_gate_enabled=bool(version_gate.get("enabled", True)),
version_gate_window=int(version_gate.get("window", 100)),
version_gate_forgejo_url=version_gate.get("forgejo_url"),
version_gate_repo_owner=version_gate.get("repo_owner"),
version_gate_repo_name=version_gate.get("repo_name"),
version_gate_branch=version_gate.get("branch", "main"),
version_gate_auth_token=version_gate.get("auth_token"),
version_gate_local_repo=Path(local_repo).resolve() if local_repo else None,
)