1dab24eef0
Integrates the multi-project work so far — the §22 design drafts, M1 (the schema spine), and M2 (project-scoped authorization + the §22.7 resolver) — on top of the v0.32.0 retire/§13 changes that landed on main meanwhile. Integration decisions: - Renumbered the M1 projects migration 025_projects.sql -> 026_projects.sql. v0.32.0 shipped 025_retired_state.sql, which rebuilds cached_rfcs and predates the project_id column; running projects *after* the retire rebuild is required so project_id survives on a fresh database (the runner applies *.sql in filename order). Comment/doc references bumped to match. - Resolved the get_rfc conflict in api.py by composing both gates: compute the viewer once, apply the §22.5 visibility gate, then v0.32.0's §13.7 retired-entry owner-only check. - api_graduation.py / api_discussion.py auto-merged cleanly (M2's threaded viewer/visibility gate coexists with the new retire endpoints + retired state). Full suite 401 passed on a fresh DB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""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
|
|
registry_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()]
|
|
|
|
# v0.18.0: `GITEA_WEBHOOK_SECRET` is now mandatory (per the
|
|
# email + webhook hygiene proposal). An empty value used to
|
|
# silently accept unsigned webhook POSTs — that was the
|
|
# invisible-failure shape the proposal targets. Now the
|
|
# framework refuses to start when the secret is empty unless
|
|
# the operator opts into the dev-bypass with
|
|
# `RFC_APP_INSECURE_WEBHOOKS=1`. Local-dev deployments without
|
|
# a wired Gitea hook set the bypass; production MUST NOT.
|
|
insecure_webhooks = os.environ.get("RFC_APP_INSECURE_WEBHOOKS", "").strip() == "1"
|
|
if insecure_webhooks:
|
|
webhook_secret = _optional("GITEA_WEBHOOK_SECRET")
|
|
else:
|
|
webhook_secret = _required("GITEA_WEBHOOK_SECRET")
|
|
|
|
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"),
|
|
# §22.2: the multi-project registry repo the framework reads to learn
|
|
# which projects exist. Optional through Slice M1 — the registry
|
|
# *mirror* lands in M3; until then the default project (seeded by
|
|
# migration 026 + the §22.13 startup backfill) is the only project,
|
|
# and its content_repo comes from META_REPO. Becomes required when
|
|
# the mirror lands and META_REPO is retired.
|
|
registry_repo=_optional("REGISTRY_REPO"),
|
|
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=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"),
|
|
)
|