G0: cell substrate — invariants made executable
Strand G0 complete and VERIFIED against real Postgres, not asserted.
- FastAPI plane, fail-closed provider seams, repair-pointer error taxonomy
- migration 001: all 10 tables incl. repo_type NOT NULL and model_cards (I-7)
- 17 invariant tests, ruff clean, vocabulary audit clean
Two bugs found by RUNNING it that review would not have caught:
1. SQLAlchemy Enum persists .name, not .value — so RepoState.deleted_soft
and CreatedVia.imported would have written labels migration 001 never
declared, failing at runtime rather than at review. Pinned via
values_callable.
2. op.create_table asks each Enum to emit its own CREATE TYPE with no
checkfirst, so the second reference raised DuplicateObject and the
migration died halfway. Types are now created once, referenced with
create_type=False.
Proven live, with the hostile env var set:
- I-12: COMMIT_SHA=deadbeef... in the environment, /version reports real HEAD.
That env pin is the documented root cause of nine sibling services
misreporting their commit; here it is structurally ignored.
- I-8: three unconfigured providers -> status degraded, HTTP 503, each saying
'refusing to report healthy'. No mock, no false green.
- G0.4: upgrade -> downgrade -> upgrade round-trip clean (10 -> 0 -> 10).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
0
api/app/providers/__init__.py
Normal file
0
api/app/providers/__init__.py
Normal file
55
api/app/providers/base.py
Normal file
55
api/app/providers/base.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Provider seams — all of them fail CLOSED (I-8).
|
||||
|
||||
`windy-cloud-sites` ships an `edge_live` gate whose whole job is "never claim
|
||||
live while the provider is mock" (commit a8ff948). `windy-cloud-domains` has a
|
||||
registrar seam that literally raises `RuntimeError("Refusing to pretend")` — and
|
||||
then shipped its public portal without wiring the equivalent gate on the quote
|
||||
route, which is why production told anyone who asked that google.com was
|
||||
available for $18.00 a year.
|
||||
|
||||
The lesson those two cells paid for: a fail-closed seam is worth nothing if a
|
||||
route can reach the data without passing through it. So here the probe and the
|
||||
gate are the SAME object, and `healthy()` can never return True for a provider
|
||||
that `configured` reports False.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProbeResult:
|
||||
ok: bool
|
||||
detail: str
|
||||
# True only when we actually reached the real dependency. A provider that is
|
||||
# merely "not configured" is ok=False, reachable=False — never ok=True.
|
||||
reachable: bool = False
|
||||
|
||||
|
||||
class Provider(abc.ABC):
|
||||
"""A dependency outside this process."""
|
||||
|
||||
name: str
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def configured(self) -> bool:
|
||||
"""Do we hold every credential needed to talk to the real thing?"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def probe(self) -> ProbeResult:
|
||||
"""Reach the real dependency. Never simulate."""
|
||||
|
||||
async def healthy(self) -> ProbeResult:
|
||||
if not self.configured:
|
||||
return ProbeResult(
|
||||
ok=False,
|
||||
detail=f"{self.name} is not configured; refusing to report healthy",
|
||||
reachable=False,
|
||||
)
|
||||
try:
|
||||
return await self.probe()
|
||||
except Exception as exc: # noqa: BLE001 - a probe must never raise upward
|
||||
return ProbeResult(ok=False, detail=f"{self.name} probe failed: {exc}")
|
||||
128
api/app/providers/registry.py
Normal file
128
api/app/providers/registry.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Concrete providers: Gitea, R2, Eternitas, Postgres, tunnel.
|
||||
|
||||
Each is a real probe against the real dependency (I-8). None of them has a mock
|
||||
mode. If you find yourself adding one, add it behind `configured` returning False
|
||||
instead — an unconfigured provider is honest; a mock provider is a liar with a
|
||||
green light.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from api.app.config import Settings
|
||||
from api.app.providers.base import ProbeResult, Provider
|
||||
|
||||
_TIMEOUT = httpx.Timeout(5.0, connect=3.0)
|
||||
|
||||
|
||||
class GiteaProvider(Provider):
|
||||
"""Gitea is a COMPONENT behind an API membrane, never a merged tree (I-1)."""
|
||||
|
||||
name = "gitea"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._s = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return self._s.gitea_configured
|
||||
|
||||
async def probe(self) -> ProbeResult:
|
||||
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
||||
r = await client.get(
|
||||
f"{self._s.gitea_base_url}/api/v1/version",
|
||||
headers={"Authorization": f"token {self._s.gitea_admin_token}"},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return ProbeResult(False, f"gitea /api/v1/version -> {r.status_code}", True)
|
||||
return ProbeResult(True, f"gitea {r.json().get('version', '?')}", True)
|
||||
|
||||
|
||||
class R2Provider(Provider):
|
||||
"""I-3: LFS, releases, artifacts, archives. NEVER git object stores."""
|
||||
|
||||
name = "r2"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._s = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return self._s.r2_configured
|
||||
|
||||
async def probe(self) -> ProbeResult:
|
||||
# HEAD the bucket via the S3 endpoint. boto3 is sync, so we keep the
|
||||
# probe to a plain reachability check here and let G4 wire the signed
|
||||
# client; a 400/403 still proves the endpoint is real and answering.
|
||||
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
||||
r = await client.head(f"{self._s.r2_endpoint_url}/{self._s.r2_bucket_lfs}")
|
||||
reachable = r.status_code < 500
|
||||
return ProbeResult(
|
||||
ok=r.status_code in (200, 400, 403),
|
||||
detail=f"r2 endpoint -> {r.status_code}",
|
||||
reachable=reachable,
|
||||
)
|
||||
|
||||
|
||||
class EternitasProvider(Provider):
|
||||
"""The one issuer. Every agent identity in the ecosystem terminates here."""
|
||||
|
||||
name = "eternitas"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._s = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return self._s.eternitas_configured
|
||||
|
||||
async def probe(self) -> ProbeResult:
|
||||
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
||||
r = await client.get(f"{self._s.eternitas_base_url}/health")
|
||||
return ProbeResult(r.status_code == 200, f"eternitas /health -> {r.status_code}", True)
|
||||
|
||||
|
||||
class DatabaseProvider(Provider):
|
||||
"""Postgres is truth. Gitea's own DB is a component's private state."""
|
||||
|
||||
name = "db"
|
||||
|
||||
def __init__(self, engine: AsyncEngine | None) -> None:
|
||||
self._engine = engine
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return self._engine is not None
|
||||
|
||||
async def probe(self) -> ProbeResult:
|
||||
assert self._engine is not None
|
||||
async with self._engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
return ProbeResult(True, "postgres reachable", True)
|
||||
|
||||
|
||||
class TunnelProvider(Provider):
|
||||
"""cloudflared is the only ingress. No inbound port is ever opened (G1.2)."""
|
||||
|
||||
name = "tunnel"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._s = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
# The tunnel is a host-level concern, not a credential we hold, so there
|
||||
# is nothing to "configure" here. The probe alone decides health, and in
|
||||
# dev it will honestly say cloudflared is not running (I-8).
|
||||
return True
|
||||
|
||||
async def probe(self) -> ProbeResult:
|
||||
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
||||
try:
|
||||
r = await client.get("http://localhost:2000/metrics")
|
||||
except httpx.RequestError as exc:
|
||||
return ProbeResult(False, f"cloudflared metrics unreachable: {exc}")
|
||||
return ProbeResult(r.status_code == 200, f"cloudflared metrics -> {r.status_code}", True)
|
||||
Reference in New Issue
Block a user