Slice 1: scaffolding + propose-to-super-draft vertical
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the §5 schema (six numbered migrations), Gitea OAuth + §6 user provisioning, the §7 catalog left pane, and the propose-to-merge vertical: propose modal opens an idea PR against the meta repo, an owner merges from the pending-idea view, the cache picks it up via webhook or reconciler sweep, and the catalog renders the new super-draft. Per §1 the bot is the only Git writer; every commit, branch creation, and PR merge carries the §6.5 On-behalf-of: trailer and an `actions` audit row. Per §4 the cache is never written from a user action — it's webhook+reconciler only. Covered by `backend/tests/test_propose_vertical.py` against an in-process Gitea simulator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Environment-derived configuration.
|
||||
|
||||
Loaded once at process start. Every module that needs a value pulls it from
|
||||
here rather than re-reading os.environ, so there is one obvious place to
|
||||
look when a setting is missing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _required(name: str) -> str:
|
||||
value = os.environ.get(name, "").strip()
|
||||
if not value:
|
||||
raise RuntimeError(f"Required environment variable {name} is not set")
|
||||
return value
|
||||
|
||||
|
||||
def _optional(name: str, default: str = "") -> str:
|
||||
return os.environ.get(name, default).strip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
gitea_url: str
|
||||
gitea_bot_user: str
|
||||
gitea_bot_token: str
|
||||
gitea_org: str
|
||||
meta_repo: str
|
||||
oauth_client_id: str
|
||||
oauth_client_secret: str
|
||||
app_url: str
|
||||
secret_key: str
|
||||
database_path: Path
|
||||
owner_gitea_login: str
|
||||
webhook_secret: str
|
||||
enabled_models: list[str] = field(default_factory=list)
|
||||
anthropic_api_key: str = ""
|
||||
google_api_key: str = ""
|
||||
openai_api_key: str = ""
|
||||
|
||||
@property
|
||||
def redirect_uri(self) -> str:
|
||||
return f"{self.app_url}/auth/callback"
|
||||
|
||||
@property
|
||||
def meta_repo_full(self) -> str:
|
||||
return f"{self.gitea_org}/{self.meta_repo}"
|
||||
|
||||
|
||||
def load_config() -> Config:
|
||||
database_path = Path(_optional("DATABASE_PATH", "data/rfc-app.db")).resolve()
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
enabled = [m.strip() for m in _optional("ENABLED_MODELS", "claude").split(",") if m.strip()]
|
||||
|
||||
return Config(
|
||||
gitea_url=_required("GITEA_URL").rstrip("/"),
|
||||
gitea_bot_user=_required("GITEA_BOT_USER"),
|
||||
gitea_bot_token=_required("GITEA_BOT_TOKEN"),
|
||||
gitea_org=_required("GITEA_ORG"),
|
||||
meta_repo=_optional("META_REPO", "meta"),
|
||||
oauth_client_id=_required("OAUTH_CLIENT_ID"),
|
||||
oauth_client_secret=_required("OAUTH_CLIENT_SECRET"),
|
||||
app_url=_optional("APP_URL", "http://localhost:8000").rstrip("/"),
|
||||
secret_key=_required("SECRET_KEY"),
|
||||
database_path=database_path,
|
||||
owner_gitea_login=_optional("OWNER_GITEA_LOGIN"),
|
||||
webhook_secret=_optional("GITEA_WEBHOOK_SECRET"),
|
||||
enabled_models=enabled,
|
||||
anthropic_api_key=_optional("ANTHROPIC_API_KEY"),
|
||||
google_api_key=_optional("GOOGLE_API_KEY"),
|
||||
openai_api_key=_optional("OPENAI_API_KEY"),
|
||||
)
|
||||
Reference in New Issue
Block a user