From dbbc264a0a0a3940bd6e94381abf6d89c8f82816 Mon Sep 17 00:00:00 2001 From: Lasse Server Date: Mon, 3 Aug 2026 08:31:45 +0200 Subject: [PATCH] Keep deployment identity out of upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site copy now resolves through CONTENT_DIR, alongside PARLIAMENT_CONFIG and PROMPTS_DIR. All three let a deployment keep its own wording, branding and contact details outside the repository, so they never appear in a diff against upstream — which is what keeps a production fork mergeable. parliament.yaml's site.title was "rixdagen.se". That is the deployment's identity, not Sweden's data, so upstream now ships a neutral default. The Swedish user guide keeps its content but loses the personal contact details and site-specific wording; a deployment ships its own guide by pointing CONTENT_DIR at it. Fixes /api/guide, which still read ../user-guide.md after that file moved to content/sv/ and had been returning 404. Deliberately kept: the author metadata in pyproject.toml and the copyright line in LICENSE, which are attribution rather than configuration, and the README's note that this runs in production as rixdagen.se, which is provenance worth stating. Co-Authored-By: Claude Opus 5 --- .env.example | 5 +++-- README.md | 13 +++++++++++-- backend/app.py | 15 +++++++++------ bootstrap.py | 4 ++-- content/sv/user-guide.md | 10 +++++----- docs/eval-harness.md | 4 ++-- parliament.py | 7 ++++++- parliament.yaml | 11 ++++++++--- 8 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index b5f12e9..4fe37a7 100644 --- a/.env.example +++ b/.env.example @@ -60,10 +60,11 @@ AUTH_PRELOGIN_SECRET= # download to reuse it rather than fetching again. PLENUM_DATA_DIR= -# Use a parliament.yaml / prompt tree outside the repository, so a deployment's -# own values never show up in a diff against upstream. +# Point these outside the repository and a deployment's own values never show up +# in a diff against upstream — config, prompts and site copy all overridable. # PARLIAMENT_CONFIG=/etc/plenum/parliament.yaml # PROMPTS_DIR=/etc/plenum/prompts +# CONTENT_DIR=/etc/plenum/content # Re-read prompt files on every call. Development only. # PROMPTS_RELOAD=1 diff --git a/README.md b/README.md index 4ae5d85..cfe0849 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,20 @@ deploy/examples/ systemd units and an nginx site, with placeholders All settings are environment variables, documented in `.env.example`. Two paths let a deployment keep its own values outside the repository entirely: -- `PARLIAMENT_CONFIG` — path to a `parliament.yaml` elsewhere on disk -- `PROMPTS_DIR` — path to a prompt tree elsewhere on disk +- `PARLIAMENT_CONFIG` — a `parliament.yaml` elsewhere on disk +- `PROMPTS_DIR` — a prompt tree elsewhere on disk +- `CONTENT_DIR` — site copy (explainer, guide) elsewhere on disk Set `PROMPTS_RELOAD=1` in development to re-read prompt files on every call. +### Running a fork + +If you maintain a deployment as a fork, keep it differing from upstream only in +files upstream does not have. Everything branded or private is either an env-var +pointer (the three above), an untracked file (`.env`, `providers.yaml`), or an +addition under `deploy/prod/`. `make check-fork-divergence` fails the build if +anything else drifts, which turns a merge conflict into a caught mistake. + ## Security The `database_query` tool executes model-authored SQL. Give it a database role with diff --git a/backend/app.py b/backend/app.py index 4db9a0f..4011cca 100644 --- a/backend/app.py +++ b/backend/app.py @@ -110,12 +110,15 @@ def _reap_abandoned_jobs() -> None: @app.get("/api/guide", response_class=PlainTextResponse) def get_guide() -> str: - """Serve user-guide.md as plain text. Single source of truth for all guide links.""" - guide_path = os.path.join(os.path.dirname(__file__), "..", "user-guide.md") - if not os.path.exists(guide_path): - raise HTTPException(status_code=404, detail="Guide not found") - with open(guide_path, encoding="utf-8") as f: - return f.read() + """Serve the user guide as plain text — the single source for all guide links. + + Resolved through CONTENT_DIR, so a deployment can ship its own guide without + editing the repository. + """ + guide = PARLIAMENT.read_content("guide_file") + if not guide: + raise HTTPException(status_code=404, detail="No guide configured") + return guide @app.get("/api/meta") diff --git a/bootstrap.py b/bootstrap.py index c8420da..8714c24 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -5,8 +5,8 @@ the repository root is not on ``sys.path`` and relative paths would resolve agai whatever directory the caller happened to be in. Importing this module fixes both, deriving the root from this file's own location. -The predecessor hardcoded ``/home/lasse/riksdagen`` in 22 files, which is why the -project could only ever run on one machine. +The predecessor hardcoded one absolute path in 22 files, which is why it could +only ever run on a single machine. """ from __future__ import annotations diff --git a/content/sv/user-guide.md b/content/sv/user-guide.md index 5640d0f..af50f8b 100644 --- a/content/sv/user-guide.md +++ b/content/sv/user-guide.md @@ -4,11 +4,11 @@ Den här guiden förklarar hur Riksdagen-appen är byggd, vilka val som gjorts, --- -## Om rixdagen.se +## Om den här sajten -Sajten är utvecklad av [Lasse Edfast](https://lasseedfast.se), hittills utan ekonomiskt stöd. Den är gratis att använda och ingen information sparas utöver de sju dagarna som en chatt sparas i. +Sajten drivs utan ekonomiskt stöd. Den är gratis att använda och ingen information sparas utöver de sju dagarna som en chatt sparas i. -Vill du vara med och utveckla, eller bara har frågor, kontakta gärna Lasse via [e-post](mailto:lasse@edfast.se). +Vill du vara med och utveckla, eller bara har frågor, hör gärna av dig. --- @@ -207,11 +207,11 @@ En API-nyckel är ett lösenord som identifierar dig mot en AI-leverantörs serv - **OpenRouter** – [openrouter.ai](https://openrouter.ai) (aggregator med tillgång till hundratals modeller) - **OpenAI** – [platform.openai.com](https://platform.openai.com) (samma modeller som driver ChatGPT) -Användning av externa modeller kostar pengar och debiteras direkt från ditt konto hos leverantören – inte via rixdagen.se. +Användning av externa modeller kostar pengar och debiteras direkt från ditt konto hos leverantören – inte via den här sajten. ### Hur nyckeln lagras -Din nyckel sparas **enbart lokalt i din webbläsare** (`localStorage`). Den skickas till rixdagen.se:s server bara för att vidarebefordras till leverantören när ett anrop görs – den loggas inte, sparas inte i databasen och syns aldrig i någon annan användares session. +Din nyckel sparas **enbart lokalt i din webbläsare** (`localStorage`). Den skickas till sajtens server bara för att vidarebefordras till leverantören när ett anrop görs – den loggas inte, sparas inte i databasen och syns aldrig i någon annan användares session. Konkret: - Nyckeln ligger kvar i webbläsaren tills du rensar den (knappen ✕ i inställningspanelen) eller tömmer webbläsarens lagringsdata. diff --git a/docs/eval-harness.md b/docs/eval-harness.md index e8343fc..61efec5 100644 --- a/docs/eval-harness.md +++ b/docs/eval-harness.md @@ -97,12 +97,12 @@ via sigmoid so 0.5 = neutral, >0.7 = likely grounded, <0.3 = likely hallucinated `NULL` means the scorer endpoint was unreachable when the judgment was recorded. -Start the scorer (first run downloads the model to `/home/lasse/models`): +Start the scorer (first run downloads the model to `$HOME/models`): ```bash nohup vllm serve BAAI/bge-reranker-v2-m3 \ --port 8001 \ - --download-dir /home/lasse/models \ + --download-dir $HOME/models \ --gpu-memory-utilization 0.2 \ --max-model-len 8192 \ --trust-remote-code \ diff --git a/parliament.py b/parliament.py index bfae36e..cee4fd0 100644 --- a/parliament.py +++ b/parliament.py @@ -24,6 +24,11 @@ import yaml _ROOT = Path(__file__).resolve().parent +# Site copy — the explainer, the limit warning, the user guide. Overridable so a +# deployment's own wording lives outside the repository, the same way PROMPTS_DIR +# works for prompts. +CONTENT_DIR = Path(os.environ.get("CONTENT_DIR") or _ROOT / "content") + # A Postgres text-search configuration name. It is interpolated into SQL rather # than passed as a parameter (identifiers cannot be bound), so it is validated # on load and never trusted from arbitrary input. @@ -129,7 +134,7 @@ class Parliament: rel = self.site.get(key) if not rel: return "" - path = _ROOT / rel + path = CONTENT_DIR / rel return path.read_text(encoding="utf-8") if path.exists() else "" # -- serialisation ------------------------------------------------------ diff --git a/parliament.yaml b/parliament.yaml index 2e05f7b..289e5a6 100644 --- a/parliament.yaml +++ b/parliament.yaml @@ -174,13 +174,18 @@ embeddings: # Served to the frontend via GET /api/meta. site: - title: rixdagen.se + # Deployment identity, not country data. Upstream ships neutral values; a + # deployment overrides them in its own PARLIAMENT_CONFIG file so its branding + # never appears in a diff against upstream. + title: plenum tagline: "Sök i riksdagens anföranden och motioner" # Markdown, so a deployment can rewrite its own copy without touching source. - explainer_file: content/sv/explainer.md - limit_warning_file: content/sv/limit_warning.md + # Paths are relative to CONTENT_DIR (default: ./content). + explainer_file: sv/explainer.md + limit_warning_file: sv/limit_warning.md # Upstream ships these empty. A deployment fills them in via its own # PARLIAMENT_CONFIG file, which keeps personal contact details out of the repo. + guide_file: sv/user-guide.md contact: email: null url: null