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/__init__.py
Normal file
0
api/app/__init__.py
Normal file
87
api/app/buildinfo.py
Normal file
87
api/app/buildinfo.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Deployment identity (I-12).
|
||||
|
||||
Nine of twelve live services in this ecosystem cannot name the commit they are
|
||||
running. One reports *another repo's* commit. The root cause found on 2026-08-10
|
||||
was hardcoded `COMMIT_SHA` pins in `/opt/*/.env` that OVERRIDE the build arg, so a
|
||||
redeploy keeps reporting the old sha until a human hand-edits the file.
|
||||
|
||||
The fix here is structural, not procedural:
|
||||
|
||||
* the sha is baked into the image at build time (Docker ARG -> this module);
|
||||
* a runtime `COMMIT_SHA` environment variable is **IGNORED**, loudly;
|
||||
* in a dev worktree with nothing baked, we read git directly and SAY SO in
|
||||
`source`, rather than reporting a value we cannot stand behind (I-8).
|
||||
|
||||
`make check` fails if /version disagrees with `git rev-parse HEAD` in CI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Rewritten at image build time by the Dockerfile. Do not edit by hand, and do
|
||||
# not "helpfully" default it to something plausible.
|
||||
BAKED_COMMIT_SHA: str = ""
|
||||
BAKED_BUILT_AT: str = ""
|
||||
|
||||
VERSION = "0.1.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildInfo:
|
||||
version: str
|
||||
commit_sha: str | None
|
||||
built_at: str | None
|
||||
source: str # "baked" | "git-worktree" | "unknown"
|
||||
|
||||
|
||||
def _git_head() -> str | None:
|
||||
try:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
return out.stdout.strip() or None if out.returncode == 0 else None
|
||||
except Exception: # pragma: no cover - defensive
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_build_info() -> BuildInfo:
|
||||
override = os.environ.get("COMMIT_SHA")
|
||||
|
||||
if BAKED_COMMIT_SHA:
|
||||
if override and override != BAKED_COMMIT_SHA:
|
||||
log.warning(
|
||||
"IGNORING COMMIT_SHA environment override (%s). This service "
|
||||
"reports the sha baked into its own artifact (%s). See I-12 — an "
|
||||
"env pin overriding the build arg is exactly why nine sibling "
|
||||
"services misreport their commit.",
|
||||
override[:12],
|
||||
BAKED_COMMIT_SHA[:12],
|
||||
)
|
||||
return BuildInfo(VERSION, BAKED_COMMIT_SHA, BAKED_BUILT_AT or None, "baked")
|
||||
|
||||
head = _git_head()
|
||||
if head:
|
||||
if override:
|
||||
log.warning(
|
||||
"IGNORING COMMIT_SHA environment override (%s); reading the "
|
||||
"worktree instead.",
|
||||
override[:12],
|
||||
)
|
||||
return BuildInfo(VERSION, head, None, "git-worktree")
|
||||
|
||||
# Nothing baked, no git. Say so. Do not invent a sha (I-8).
|
||||
return BuildInfo(VERSION, None, None, "unknown")
|
||||
119
api/app/config.py
Normal file
119
api/app/config.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Settings for the windy-git plane.
|
||||
|
||||
Every `env:` default in DNA_STRAND_MASTER_PLAN.md is shipped here as an actual
|
||||
default, not a suggestion. Providers are FAIL-CLOSED (I-8): a provider whose
|
||||
credentials are absent reports itself unconfigured and refuses to answer, rather
|
||||
than answering from a mock. The domains cell shipped a portal on a mock registrar
|
||||
and told the public that google.com was available for $18.00 a year. That failure
|
||||
mode is banned here by construction.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env", env_file_encoding="utf-8", extra="ignore"
|
||||
)
|
||||
|
||||
# ---- service identity -------------------------------------------------
|
||||
environment: str = "development"
|
||||
service_name: str = "windy-git"
|
||||
port: int = 8600
|
||||
|
||||
# ---- database ---------------------------------------------------------
|
||||
# Postgres schema `windgit`, own alembic (G0.4).
|
||||
database_url: str = "postgresql+asyncpg://windygit:windygit@localhost:5432/windygit"
|
||||
db_schema: str = "windgit"
|
||||
|
||||
# ---- Gitea (a COMPONENT behind an API membrane, never a merged tree) ---
|
||||
gitea_base_url: str = "http://localhost:3000"
|
||||
gitea_admin_token: str = ""
|
||||
|
||||
# ---- Cloudflare R2 (I-3: heavy bytes only, never git objects) ---------
|
||||
r2_account_id: str = ""
|
||||
r2_access_key_id: str = ""
|
||||
r2_secret_access_key: str = ""
|
||||
r2_bucket_lfs: str = "windy-git-lfs"
|
||||
r2_bucket_artifacts: str = "windy-git-artifacts"
|
||||
r2_bucket_backups: str = "windy-git-backups"
|
||||
|
||||
# ---- Eternitas (agent identity + trust) -------------------------------
|
||||
eternitas_base_url: str = "https://api.eternitas.ai"
|
||||
eternitas_platform_api_key: str = ""
|
||||
|
||||
# ---- account-server OIDC (human identity) -----------------------------
|
||||
account_server_base_url: str = "https://account.windyword.ai"
|
||||
|
||||
# ---- storage law (I-3, G4.4) ------------------------------------------
|
||||
# Git object databases MUST live on a POSIX filesystem. A test asserts this
|
||||
# path does not resolve to a network mount.
|
||||
git_data_root: str = "/srv/windygit/git"
|
||||
|
||||
# ---- LFS threshold (G4.5) ---------------------------------------------
|
||||
# Small text files stay in git proper. LFS-for-everything makes clones slow
|
||||
# and operations heavy.
|
||||
lfs_threshold_bytes: int = 5 * 1024 * 1024 # env: 5 MB
|
||||
lfs_extensions: tuple[str, ...] = (
|
||||
".safetensors", ".bin", ".gguf", ".pt", ".ckpt", ".onnx",
|
||||
".zip", ".tar", ".gz", ".mp4", ".wav", ".mov", ".psd",
|
||||
)
|
||||
|
||||
# ---- velocity bases, multiplied by EI band (G3.4) ---------------------
|
||||
# Platinum x10 / Gold x4 / Standard x1 / Watch x0.5 / Untrusted read-only
|
||||
rate_pushes_per_day: int = 500
|
||||
rate_repo_creates_per_day: int = 50
|
||||
rate_grants_per_day: int = 100
|
||||
rate_force_pushes_per_day: int = 10
|
||||
|
||||
# ---- mirror health (I-4) ----------------------------------------------
|
||||
mirror_lag_p2_seconds: int = 3600 # env: 60 min -> P2
|
||||
|
||||
# ---- agent grants (G5.3) ----------------------------------------------
|
||||
agent_grant_default_days: int = 90
|
||||
|
||||
# ---- repo types (I-7: first-class from migration 001) -----------------
|
||||
repo_types_enabled: tuple[str, ...] = ("code",) # model/dataset are v2
|
||||
|
||||
# ---- feature gates ----------------------------------------------------
|
||||
# Mirrors the sibling `edge_live` pattern (windy-cloud-sites, a8ff948):
|
||||
# never claim live while a provider is mock.
|
||||
hf_compat_enabled: bool = False # Grant-gated, DNA plan section 7.7
|
||||
|
||||
kit0_host: str = Field(
|
||||
default="72.60.118.54",
|
||||
description="Recorded ONLY so the G1 guard can refuse to deploy here (D-4).",
|
||||
)
|
||||
|
||||
# ---- derived ----------------------------------------------------------
|
||||
@property
|
||||
def r2_configured(self) -> bool:
|
||||
return bool(
|
||||
self.r2_account_id and self.r2_access_key_id and self.r2_secret_access_key
|
||||
)
|
||||
|
||||
@property
|
||||
def gitea_configured(self) -> bool:
|
||||
return bool(self.gitea_base_url and self.gitea_admin_token)
|
||||
|
||||
@property
|
||||
def eternitas_configured(self) -> bool:
|
||||
return bool(self.eternitas_base_url and self.eternitas_platform_api_key)
|
||||
|
||||
@property
|
||||
def r2_endpoint_url(self) -> str:
|
||||
return f"https://{self.r2_account_id}.r2.cloudflarestorage.com"
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.environment.lower() in {"production", "prod"}
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
108
api/app/errors.py
Normal file
108
api/app/errors.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Repair-pointer error taxonomy (section 0.6, G8.3).
|
||||
|
||||
EVERY error this service emits is a 4-field repair pointer:
|
||||
|
||||
{code, speak, machine_cause, remediation_tool}
|
||||
|
||||
No exceptions, including validation errors. `speak` is grandma-words (I-9) and
|
||||
obeys the D-9 vocabulary law (see scripts/vocab_audit.py), and never uses the
|
||||
word "commit" on a shelter surface.
|
||||
`remediation_tool` names the tool an agent should call next, or null when a human
|
||||
must act.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
class RepairPointer(HTTPException):
|
||||
"""An error an agent can act on without guessing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int,
|
||||
code: str,
|
||||
speak: str,
|
||||
machine_cause: str,
|
||||
remediation_tool: str | None = None,
|
||||
**extra: Any,
|
||||
) -> None:
|
||||
self.code = code
|
||||
self.speak = speak
|
||||
self.machine_cause = machine_cause
|
||||
self.remediation_tool = remediation_tool
|
||||
super().__init__(
|
||||
status_code=status_code,
|
||||
detail={
|
||||
"code": code,
|
||||
"speak": speak,
|
||||
"machine_cause": machine_cause,
|
||||
"remediation_tool": remediation_tool,
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def provider_unconfigured(provider: str, missing: str) -> RepairPointer:
|
||||
"""I-8: fail closed. Never answer from a mock, never claim live.
|
||||
|
||||
This is the guard the domains cell did not have on its public quote route.
|
||||
"""
|
||||
return RepairPointer(
|
||||
status_code=503,
|
||||
code="provider_unconfigured",
|
||||
speak=(
|
||||
"That part of Windy Git isn't switched on yet, so we're not going to "
|
||||
"guess at an answer. Nothing you have is affected."
|
||||
),
|
||||
machine_cause=f"{provider} is not configured: {missing} is unset",
|
||||
remediation_tool=None,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
|
||||
def passport_unresolvable(passport: str, upstream_status: int) -> RepairPointer:
|
||||
"""G3.6: 404 and 400 REFUSE. There is no soft-allow path here.
|
||||
|
||||
windy-chat maps 400/429 to "unreachable" and then soft-ALLOWS, which is a
|
||||
live residual bypass because 429 is trivially inducible at 100/min/IP.
|
||||
"""
|
||||
return RepairPointer(
|
||||
status_code=403,
|
||||
code="passport_unresolvable",
|
||||
speak="We couldn't confirm that helper's ID, so we didn't let it make changes.",
|
||||
machine_cause=(
|
||||
f"eternitas trust lookup for {passport} returned {upstream_status}; "
|
||||
"policy is REFUSE on 400/404 and REFUSE after backoff on 429/5xx"
|
||||
),
|
||||
remediation_tool="windy_git.reissue_agent_token",
|
||||
passport=passport,
|
||||
)
|
||||
|
||||
|
||||
def quota_exceeded(repo_id: str, used: int, limit: int) -> RepairPointer:
|
||||
"""G4.6: we emit the cross-sell hook; the KERNEL owns the price (I-11)."""
|
||||
return RepairPointer(
|
||||
status_code=413,
|
||||
code="quota_exceeded",
|
||||
speak=(
|
||||
"Your projects have outgrown the space on your plan. Nothing was lost — "
|
||||
"the newest save just didn't go through."
|
||||
),
|
||||
machine_cause=f"repo {repo_id} would use {used} bytes against a limit of {limit}",
|
||||
remediation_tool="windy_cloud.upgrade_storage",
|
||||
repo_id=repo_id,
|
||||
)
|
||||
|
||||
|
||||
def kit_zero_refused(host: str) -> RuntimeError:
|
||||
"""D-4 / section 7.8: only Grant may overturn a never."""
|
||||
return RuntimeError(
|
||||
f"REFUSING TO START: this service is pointed at Kit 0 ({host}). "
|
||||
"Windy Git never runs on Kit 0 — CI executes untrusted code and Kit 0 "
|
||||
"holds identity, the certificate authority, mail, Matrix and the broker. "
|
||||
"See DNA_STRAND_MASTER_PLAN.md D-4."
|
||||
)
|
||||
132
api/app/main.py
Normal file
132
api/app/main.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""windy-git — the version, permission and provenance plane over Windy Cloud.
|
||||
|
||||
Strand G0. This process is OUR service. Gitea runs beside it as an unforked
|
||||
component and is reached only over its REST API (D-2 / I-1).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from api.app.buildinfo import get_build_info
|
||||
from api.app.config import get_settings
|
||||
from api.app.errors import RepairPointer, kit_zero_refused
|
||||
from api.app.providers.registry import (
|
||||
DatabaseProvider,
|
||||
EternitasProvider,
|
||||
GiteaProvider,
|
||||
R2Provider,
|
||||
TunnelProvider,
|
||||
)
|
||||
from api.app.routes import health
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='{"ts":"%(asctime)s","level":"%(levelname)s","logger":"%(name)s","msg":"%(message)s"}',
|
||||
)
|
||||
log = logging.getLogger("windy-git")
|
||||
|
||||
|
||||
def _refuse_kit_zero(settings) -> None:
|
||||
"""D-4 / section 7.8 — only Grant may overturn a never.
|
||||
|
||||
Kit 0 is disqualified on four independent grounds, any one sufficient. The
|
||||
strongest: CI executes arbitrary workflow code, and Kit 0 holds identity, the
|
||||
certificate authority, inbound SMTP, Matrix, the broker and the admin console.
|
||||
A guard in a document is a preference; a guard in the boot path is a rule.
|
||||
"""
|
||||
if not settings.is_production:
|
||||
return
|
||||
try:
|
||||
local_ips = {
|
||||
info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None)
|
||||
}
|
||||
except socket.gaierror:
|
||||
return
|
||||
if settings.kit0_host in local_ips:
|
||||
raise kit_zero_refused(settings.kit0_host)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
settings = get_settings()
|
||||
_refuse_kit_zero(settings)
|
||||
|
||||
info = get_build_info()
|
||||
log.info(
|
||||
"starting windy-git %s commit=%s source=%s env=%s",
|
||||
info.version,
|
||||
(info.commit_sha or "unknown")[:12],
|
||||
info.source,
|
||||
settings.environment,
|
||||
)
|
||||
if info.source == "unknown":
|
||||
log.warning(
|
||||
"This process cannot name its own commit. It will report null rather "
|
||||
"than guess (I-12), but a production deploy in this state is a defect."
|
||||
)
|
||||
|
||||
engine = None
|
||||
try:
|
||||
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("database engine not created: %s", exc)
|
||||
|
||||
app.state.settings = settings
|
||||
app.state.engine = engine
|
||||
app.state.providers = [
|
||||
DatabaseProvider(engine),
|
||||
GiteaProvider(settings),
|
||||
R2Provider(settings),
|
||||
EternitasProvider(settings),
|
||||
TunnelProvider(settings),
|
||||
]
|
||||
|
||||
yield
|
||||
|
||||
if engine is not None:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Windy Git",
|
||||
description=(
|
||||
"The version, permission and provenance plane over Windy Cloud. "
|
||||
"Agents are citizens here, not tourists wearing a human's token."
|
||||
),
|
||||
version=get_build_info().version,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.include_router(health.router)
|
||||
|
||||
|
||||
@app.exception_handler(RepairPointer)
|
||||
async def _repair_pointer_handler(_, exc: RepairPointer) -> JSONResponse:
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.detail)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def _validation_handler(_, exc: RequestValidationError) -> JSONResponse:
|
||||
"""G8.3: EVERY error is a repair pointer. Including validation errors.
|
||||
|
||||
FastAPI's default 422 body is machine-readable and human-hostile. It is also
|
||||
the single most common error an agent will hit, so it is the last place to
|
||||
drop the contract.
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content={
|
||||
"code": "invalid_request",
|
||||
"speak": "Something in that request didn't look right, so we didn't act on it.",
|
||||
"machine_cause": f"request validation failed: {exc.errors()}",
|
||||
"remediation_tool": None,
|
||||
},
|
||||
)
|
||||
0
api/app/models/__init__.py
Normal file
0
api/app/models/__init__.py
Normal file
323
api/app/models/core.py
Normal file
323
api/app/models/core.py
Normal file
@@ -0,0 +1,323 @@
|
||||
"""Data model, section 4 of the DNA plan.
|
||||
|
||||
I-7 is enforced here structurally: `repo_type` is NOT NULL from migration 001,
|
||||
and `model_cards` exists in v1 even though `repo_type=model` does not ship until
|
||||
v2. Shipping v1 with the v2 columns absent is forbidden — cheap now,
|
||||
near-impossible to retrofit.
|
||||
|
||||
Postgres is truth. Gitea's own database is a component's private state and this
|
||||
service never writes to it directly (I-1).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
ARRAY,
|
||||
BigInteger,
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
SCHEMA = "windgit"
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class RepoType(enum.StrEnum):
|
||||
"""I-7 / D-6. A Hugging Face repo IS a git repo with LFS and a model card —
|
||||
the consolidation is metadata and UI, not infrastructure."""
|
||||
|
||||
code = "code"
|
||||
model = "model"
|
||||
dataset = "dataset"
|
||||
|
||||
|
||||
class Visibility(enum.StrEnum):
|
||||
private = "private"
|
||||
unlisted = "unlisted"
|
||||
public = "public"
|
||||
|
||||
|
||||
class RepoState(enum.StrEnum):
|
||||
active = "active"
|
||||
archived = "archived"
|
||||
deleted_soft = "deleted-soft"
|
||||
|
||||
|
||||
class CreatedVia(enum.StrEnum):
|
||||
portal = "portal"
|
||||
agent = "agent"
|
||||
imported = "import"
|
||||
cloud_folder = "cloud-folder"
|
||||
|
||||
|
||||
class GrantRole(enum.StrEnum):
|
||||
owner = "owner"
|
||||
maintainer = "maintainer"
|
||||
writer = "writer"
|
||||
reader = "reader"
|
||||
|
||||
|
||||
class MirrorState(enum.StrEnum):
|
||||
healthy = "healthy"
|
||||
degraded = "degraded"
|
||||
failed = "failed"
|
||||
|
||||
|
||||
|
||||
def _pg_enum(enum_cls, name: str):
|
||||
"""SQLAlchemy's Enum persists `.name` by default, not `.value`.
|
||||
|
||||
Three members here have a name that differs from its value
|
||||
(`deleted_soft`/"deleted-soft", `imported`/"import", `cloud_folder`/
|
||||
"cloud-folder"), so the default would write a label migration 001 does not
|
||||
declare and the insert would fail at runtime, not at review. Pin values
|
||||
explicitly.
|
||||
"""
|
||||
return Enum(
|
||||
enum_cls,
|
||||
name=name,
|
||||
schema=SCHEMA,
|
||||
values_callable=lambda cls: [member.value for member in cls],
|
||||
)
|
||||
|
||||
|
||||
def _pk() -> Mapped[uuid.UUID]:
|
||||
return mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
|
||||
class Repo(Base):
|
||||
__tablename__ = "repos"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("identity_id", "slug", name="uq_repos_identity_slug"),
|
||||
Index("ix_repos_repo_type", "repo_type"),
|
||||
Index("ix_repos_passport", "passport"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
identity_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# Agent-owned repos. An agent is a citizen here, not a guest on a human's row.
|
||||
passport: Mapped[str | None] = mapped_column(String(32))
|
||||
slug: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
|
||||
# I-7: first-class, NOT NULL, never inferred, never defaulted at read time.
|
||||
repo_type: Mapped[RepoType] = mapped_column(
|
||||
_pg_enum(RepoType, "repo_type"), nullable=False
|
||||
)
|
||||
|
||||
gitea_repo_id: Mapped[int | None] = mapped_column(Integer)
|
||||
visibility: Mapped[Visibility] = mapped_column(
|
||||
_pg_enum(Visibility, "visibility"),
|
||||
nullable=False,
|
||||
default=Visibility.private,
|
||||
)
|
||||
# D-8: set when this repo was git-enabled from a Windy Cloud folder. The
|
||||
# user's files stay first-class Cloud objects; we never take custody (I-13).
|
||||
cloud_folder_ref: Mapped[str | None] = mapped_column(String(255))
|
||||
default_branch: Mapped[str] = mapped_column(String(128), default="main")
|
||||
lfs_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
object_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
state: Mapped[RepoState] = mapped_column(
|
||||
_pg_enum(RepoState, "repo_state"), default=RepoState.active
|
||||
)
|
||||
created_via: Mapped[CreatedVia] = mapped_column(
|
||||
_pg_enum(CreatedVia, "created_via"), nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class RepoGrant(Base):
|
||||
"""The shelter (D-8/G5.3). Windy Cloud has no sharing of any kind today —
|
||||
verified 2026-08-11 against routes/storage.py and its models."""
|
||||
|
||||
__tablename__ = "repo_grants"
|
||||
__table_args__ = (
|
||||
# Exactly one grantee. Enforced by the database, not by application code,
|
||||
# because application-enforced invariants are how this ecosystem got a
|
||||
# double-mint and a unique constraint living in two files.
|
||||
CheckConstraint(
|
||||
"(grantee_identity_id IS NULL) <> (grantee_passport IS NULL)",
|
||||
name="ck_grant_exactly_one_grantee",
|
||||
),
|
||||
Index("ix_grants_repo", "repo_id"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
grantee_identity_id: Mapped[str | None] = mapped_column(String(64))
|
||||
grantee_passport: Mapped[str | None] = mapped_column(String(32))
|
||||
role: Mapped[GrantRole] = mapped_column(
|
||||
_pg_enum(GrantRole, "grant_role"), nullable=False
|
||||
)
|
||||
granted_by: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# Agent grants expire by default (env: 90 days). A permanent agent credential
|
||||
# is a standing liability nobody chose.
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
confirm_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class RepoVersion(Base):
|
||||
"""Our own view of history, independent of Gitea (I-1)."""
|
||||
|
||||
__tablename__ = "repo_versions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("repo_id", "seq", name="uq_version_repo_seq"),
|
||||
Index("ix_versions_commit", "commit_sha"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
seq: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
commit_sha: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
tree_sha: Mapped[str | None] = mapped_column(String(64))
|
||||
author_identity_id: Mapped[str | None] = mapped_column(String(64))
|
||||
author_passport: Mapped[str | None] = mapped_column(String(32))
|
||||
signed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
signature_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
# G9.1: frozen at push time, NEVER recomputed. A band is a statement about
|
||||
# what was known then, not a live lookup.
|
||||
ei_at_action: Mapped[str | None] = mapped_column(String(32))
|
||||
message: Mapped[str | None] = mapped_column(Text)
|
||||
bytes_added: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class Mirror(Base):
|
||||
"""I-4: never a one-way door. This table is what the alerting reads."""
|
||||
|
||||
__tablename__ = "mirror_state"
|
||||
__table_args__ = ({"schema": SCHEMA},)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
remote_url: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
direction: Mapped[str] = mapped_column(String(16), default="push")
|
||||
last_success_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
lag_seconds: Mapped[int | None] = mapped_column(Integer)
|
||||
state: Mapped[MirrorState] = mapped_column(
|
||||
_pg_enum(MirrorState, "mirror_health"), default=MirrorState.healthy
|
||||
)
|
||||
|
||||
|
||||
class AgentToken(Base):
|
||||
"""G6.3. Never a human's PAT wearing an agent's name — that is the entire
|
||||
GitHub grievance and the reason this product exists."""
|
||||
|
||||
__tablename__ = "agent_tokens"
|
||||
__table_args__ = (
|
||||
Index("ix_agent_tokens_passport", "passport"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
passport: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
repo_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE")
|
||||
)
|
||||
scopes: Mapped[list[str]] = mapped_column(ARRAY(String(64)), nullable=False)
|
||||
# We store a hash. Never the token.
|
||||
token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
revoked_reason: Mapped[str | None] = mapped_column(String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class AgentAction(Base):
|
||||
__tablename__ = "agent_actions"
|
||||
__table_args__ = (
|
||||
Index("ix_agent_actions_passport_ts", "passport", "ts"),
|
||||
{"schema": SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
passport: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
repo_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
action: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
ei_at_action: Mapped[str | None] = mapped_column(String(32))
|
||||
confirm_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
result: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
# G3.7: actually populated. windy-chat emits nothing here and contributes $0
|
||||
# to the cost dashboard despite real spend.
|
||||
cost_microcents: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class ModelCard(Base):
|
||||
"""v2 surface, v1 schema (I-7). Populated from README.md YAML frontmatter."""
|
||||
|
||||
__tablename__ = "model_cards"
|
||||
__table_args__ = ({"schema": SCHEMA},)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False, unique=True
|
||||
)
|
||||
base_model: Mapped[str | None] = mapped_column(String(255))
|
||||
license: Mapped[str | None] = mapped_column(String(64))
|
||||
pipeline_tag: Mapped[str | None] = mapped_column(String(64))
|
||||
tags: Mapped[list[str] | None] = mapped_column(ARRAY(String(64)))
|
||||
library: Mapped[str | None] = mapped_column(String(64))
|
||||
card_yaml: Mapped[dict | None] = mapped_column(JSONB)
|
||||
card_body: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Job(Base):
|
||||
__tablename__ = "jobs"
|
||||
__table_args__ = ({"schema": SCHEMA},)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
kind: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
state: Mapped[str] = mapped_column(String(32), default="pending")
|
||||
attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
run_after: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class WebhookEvent(Base):
|
||||
__tablename__ = "webhook_events"
|
||||
__table_args__ = ({"schema": SCHEMA},)
|
||||
|
||||
id: Mapped[uuid.UUID] = _pk()
|
||||
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
delivered: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
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)
|
||||
0
api/app/routes/__init__.py
Normal file
0
api/app/routes/__init__.py
Normal file
75
api/app/routes/health.py
Normal file
75
api/app/routes/health.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""G0.2 / G0.3 — /version and /health/full.
|
||||
|
||||
The two endpoints this ecosystem most needs to be honest, because both audits
|
||||
found them lying elsewhere: nine of twelve services cannot name their commit, and
|
||||
a sibling cell reported healthy while serving from a mock.
|
||||
|
||||
`/health/full` returns `ok` ONLY when every provider it depends on proved itself
|
||||
against the real dependency. Anything else is `degraded`. There is no code path
|
||||
that returns `ok` from an unconfigured provider (I-8).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
|
||||
from api.app.buildinfo import get_build_info
|
||||
from api.app.config import get_settings
|
||||
|
||||
router = APIRouter(tags=["system"])
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
async def version() -> dict:
|
||||
"""I-12. A runtime COMMIT_SHA override is ignored; see buildinfo.py."""
|
||||
info = get_build_info()
|
||||
s = get_settings()
|
||||
return {
|
||||
"service": s.service_name,
|
||||
"version": info.version,
|
||||
"commit_sha": info.commit_sha,
|
||||
"built_at": info.built_at,
|
||||
"source": info.source,
|
||||
"environment": s.environment,
|
||||
"repo_types_enabled": list(s.repo_types_enabled),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health() -> dict:
|
||||
"""Cheap liveness. Says nothing about dependencies — /health/full does that."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/health/full")
|
||||
async def health_full(request: Request, response: Response) -> dict:
|
||||
providers = request.app.state.providers
|
||||
checks: dict[str, dict] = {}
|
||||
|
||||
for provider in providers:
|
||||
result = await provider.healthy()
|
||||
checks[provider.name] = {
|
||||
"ok": result.ok,
|
||||
"configured": provider.configured,
|
||||
"reachable": result.reachable,
|
||||
"detail": result.detail,
|
||||
}
|
||||
|
||||
all_ok = all(c["ok"] for c in checks.values())
|
||||
status = "ok" if all_ok else "degraded"
|
||||
if not all_ok:
|
||||
# Degraded is a real answer, not a 500. But it must never read as ok.
|
||||
response.status_code = 503
|
||||
|
||||
info = get_build_info()
|
||||
return {
|
||||
"status": status,
|
||||
"commit_sha": info.commit_sha,
|
||||
"checks": checks,
|
||||
# Grandma-words, and the D-9 vocabulary law binds this string.
|
||||
"speak": (
|
||||
"Everything is working."
|
||||
if all_ok
|
||||
else "Some parts of Windy Git aren't switched on. Nothing you have is lost."
|
||||
),
|
||||
}
|
||||
0
api/app/services/__init__.py
Normal file
0
api/app/services/__init__.py
Normal file
Reference in New Issue
Block a user