Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
2cc6ca0
docs: add revised Krater spec and Weave/SkyPilot integration docs
claude Sep 26, 2026
65b3c34
docs: one SkyPilot workspace per project, members sign in with Weave
claude Sep 26, 2026
6e917f6
docs: point to the Weave Krater-Integration branch
claude Sep 26, 2026
e6e8dc2
docs: fix remaining Weave branch reference
claude Sep 26, 2026
2d17282
docs: add CLAUDE.md with stack, layout and conventions
claude Sep 26, 2026
0ad1a69
Add project scaffolding, settings and database base
claude Sep 26, 2026
d638737
Add SQLAlchemy models for the Krater data model
claude Sep 26, 2026
64e16b4
Add Alembic, configured from Settings, plus the initial migrations
claude Sep 26, 2026
a5443a9
Add the procrastinate worker app and heartbeat task
claude Sep 26, 2026
14d7fc5
Add the FastAPI web app: create_app(), base layout, home and healthz
claude Sep 26, 2026
ef66041
Add the test suite: transactional fixtures, healthz/home and a model …
claude Sep 26, 2026
c64d1c7
Add Dockerfile, docker-compose, CI workflow and a Development README …
claude Sep 26, 2026
1b642aa
Exclude procrastinate tables from autogenerate; add shared Actor type
claude Sep 26, 2026
afc34bb
Fix line length in actor.py
claude Sep 26, 2026
453211d
Add the WeaveClient protocol, identity/user types, errors and a TTL c…
claude Sep 26, 2026
efbd180
Add LiveWeaveClient and StubWeaveClient, plus the get_weave_client() …
claude Sep 26, 2026
7fd8c28
Add upsert_user_from_identity
claude Sep 26, 2026
d02ac2f
Add Weave sign-in: /login, /auth/callback, /logout, /auth/stub, and deps
claude Sep 26, 2026
2ca37ae
Add tests for the Weave client, upsert and the sign-in flow
claude Sep 26, 2026
97df6dd
Add reviewer_groups snapshot column to reviews
claude Sep 26, 2026
c144785
Add domain errors, audit, budget, and approval-policy services
claude Sep 26, 2026
a6f560b
Reject backslashes and control characters in the post-login next path
claude Sep 26, 2026
30ad0b7
Merge wave2-auth: Weave sign-in and WeaveClient
claude Sep 26, 2026
8c34061
Add tests for the project workflow, budget, and approval-policy services
claude Sep 26, 2026
6f29b8e
Merge wave2-services: workflow services, approval policy, budget ledger
claude Sep 26, 2026
162ef2e
Prevent concurrent double-reviews with a DB constraint
claude Sep 26, 2026
0df332e
Build the web UI: proposals, review, admin overrides, and the gallery
claude Sep 26, 2026
47fd93e
Only accept http(s) repo and demo links
claude Sep 26, 2026
9091365
Add SkyPilot integration spike findings
claude Sep 26, 2026
84e5195
Add temporary SeaweedFS storage container for screenshots
claude Sep 26, 2026
f85e876
Add SkyPilot/S3 settings; stop spend snapshots tying within a transac…
claude Sep 26, 2026
288316a
Allow AuditEvent.actor_id to be null for system-actor events
claude Sep 26, 2026
283cd9b
Add SkyPilot client, sync reconciler, worker task and project-page UI
claude Sep 26, 2026
4b9c479
Add the SkyPilot admin-policy launch gate
claude Sep 26, 2026
13125e4
Add skypilot/auth-proxy compose services behind a profile
claude Sep 26, 2026
d849a4b
Correct skypilot-integration.md against spike findings, add staging r…
claude Sep 26, 2026
7742907
Merge wave-sky-policy: SkyPilot launch gate, compose services, stagin…
claude Sep 26, 2026
d4ff524
Note that the SkyPilot start command matches the official Helm chart
claude Sep 26, 2026
1738d4b
Capture final spend before workspace teardown; reorder reconcile steps
claude Sep 26, 2026
764928b
Merge wave-sky-sync: SkyPilot client, reconcile job, budget warnings
claude Sep 26, 2026
4937356
Add a deselected-by-default pytest marker for live-service tests
claude Sep 26, 2026
d52b7c0
Prove the SkyPilot integration against a real 0.13.0 server; fix 3 wi…
claude Sep 26, 2026
abef412
Merge e2e-sky: SkyPilot contract test against a real 0.13.0 server, 3…
claude Sep 26, 2026
5269a0d
test: add a repeatable live Weave e2e check
claude Sep 26, 2026
848bbfc
Merge e2e-weave: live sign-in test against a real Weave
claude Sep 26, 2026
f6c23cf
docs: describe the narrowed Weave form-action fix
claude Sep 26, 2026
299a755
Add screenshot uploads for completed projects
claude Sep 26, 2026
022be81
Add Slack integration: review channels, interactivity, and the member…
claude Sep 26, 2026
795ff2b
Merge wave-slack: Slack review channels, interactivity, membership gate
claude Sep 26, 2026
32f085f
Verify screenshot content type by magic-byte signature at confirm
claude Sep 26, 2026
9da713d
Merge wave-uploads: screenshot uploads to S3-compatible storage
claude Sep 26, 2026
9471565
docs: add FUTURE.md with post-v1 work, loose ends, and the donated-co…
claude Sep 26, 2026
e57c598
Fix: withdrawn/completed projects can no longer be approved back to life
claude Sep 26, 2026
fefa9be
Fix: budget teardown now tears down every run, not just for unseen cl…
claude Sep 26, 2026
a9c4ed4
docs: member guide to running only when compute is cheap; add spot te…
claude Sep 26, 2026
b5f63ee
Add production config guards
claude Sep 26, 2026
35d1196
Add security response headers middleware
claude Sep 26, 2026
3fd5db7
Add in-process rate limiting for public/POST routes
claude Sep 26, 2026
c4e9e3d
Add structured logging with request ids and secret scrubbing
claude Sep 26, 2026
f728fc0
Wire up hardening middleware, cookie flags, and a generic 500 page
claude Sep 26, 2026
48e6c39
Fix: cap max_hourly_cost and force autodown on any_of/ordered resourc…
claude Sep 26, 2026
1acbcd5
Merge wave-harden: security headers, cookies, rate limiting, log scru…
claude Sep 26, 2026
2ac9e80
Refuse placeholder secret keys in production
claude Sep 26, 2026
ad42889
Fix: launch gate no longer leaks project details to non-team requesters
claude Sep 26, 2026
99379ac
Fix: enforce the launch gate on every compute-provisioning request na…
claude Sep 26, 2026
36fa3af
Fix: lock the project row to stop concurrent approvals double-applyin…
claude Sep 27, 2026
cc3b30d
Fix: escape mrkdwn special characters in Slack messages
claude Sep 27, 2026
1c74541
Fix: cap custom Vast bid prices in the launch gate, not just max_hour…
claude Sep 27, 2026
e7aaebe
Merge wave-secfix: fixes for the security review findings and a Vast …
claude Sep 27, 2026
4a30ad1
Contract test: accept the gate's generic budget message; document the…
claude Sep 27, 2026
e46b702
Document the GPU pricing source decision: SkyPilot's catalog CSV, not…
claude Sep 27, 2026
7b2d26b
SkyPilot client: add list_gpu_prices(), fetching Vast's catalog CSV d…
claude Sep 27, 2026
52b3479
Add GpuPrice model, pricing service, daily refresh task and CLI
claude Sep 27, 2026
18218b1
Add the public /pricing page
claude Sep 27, 2026
2ade398
Add the budget estimator to the proposal/amendment draft form
claude Sep 27, 2026
7f67658
Pricing page: hide the spot column/option when no spot prices exist; …
claude Sep 27, 2026
187b8fb
docs: add HANDOFF.md for picking the project up in a fresh session
claude Sep 27, 2026
d7de30a
Run Krater on Weave main, with roles in Krater's own database
Adambomb210 Sep 28, 2026
e52ef3a
Polish the site UI and show real error pages to browsers
Adambomb210 Oct 7, 2026
16de2cc
feat: read Krater roles from Weave claims and directory API
jaspermayone Oct 7, 2026
206aa9e
refactor: authorize against Weave and drop Krater-side roles
jaspermayone Oct 7, 2026
f68c160
test: point the live Weave check at Weave-owned roles
jaspermayone Oct 7, 2026
d720fde
docs: Weave owns roles
jaspermayone Oct 7, 2026
ff65ad9
docs: point links at patchworklabsorg/krater
jaspermayone Oct 7, 2026
2deba17
docs: note weave roles and directory api are merged
jaspermayone Oct 7, 2026
d4a9136
docs: drop finished items from future work
jaspermayone Oct 7, 2026
7ec5fd6
Set up the screenshot bucket during migrations, not a separate container
Adambomb210 Oct 7, 2026
3925212
docs: note weave security fixes are merged in weave#119
jaspermayone Oct 7, 2026
4f130ca
Merge the UI polish and storage setup work onto Weave-owned roles
Adambomb210 Oct 7, 2026
492def2
Give the Weave e2e fixture users Krater's app roles
Adambomb210 Oct 9, 2026
7aaa1ac
Add production deployment pieces to the Compose stack
Adambomb210 Oct 9, 2026
235d87b
docs: fix stale SPEC notes on the launch gate and Weave work
Adambomb210 Oct 9, 2026
c4dadb8
Add a manual deploy workflow for alastor's NixOS module
Adambomb210 Oct 10, 2026
5877052
docs: alastor hostnames and the Cloudflare proxy count
Adambomb210 Oct 10, 2026
df3da31
docs: Slack app manifest and Tailscale Funnel as a tunnel
Adambomb210 Oct 10, 2026
3406cc3
test: provision krater app roles for live weave tests
jaspermayone Oct 10, 2026
2b54f8e
feat: remove lost members from skypilot workspaces
jaspermayone Oct 10, 2026
7ed19a1
docs: describe skypilot access that follows weave roles
jaspermayone Oct 10, 2026
807f721
Restyle the portal with Patchwork's quilt design system
Adambomb210 Oct 10, 2026
791f1c4
Skip Weave's directory cache for every action
Adambomb210 Oct 10, 2026
db89659
Show bad repo and demo links as form errors instead of a 500
Adambomb210 Oct 10, 2026
23890d3
Refuse amounts over $1,000,000 in every money field
Adambomb210 Oct 10, 2026
d7257e0
Don't let admins decide or fund their own projects
Adambomb210 Oct 10, 2026
fba1609
Refuse $0 admin budget adjustments
Adambomb210 Oct 10, 2026
66f6b66
Split the launch price cap across a task's nodes
Adambomb210 Oct 10, 2026
673e134
Deny the slurm and verda clouds in SkyPilot workspaces
Adambomb210 Oct 10, 2026
acb6992
Only show launch instructions while launches are allowed
Adambomb210 Oct 10, 2026
66927fd
Give every caller the same launch-gate reject messages
Adambomb210 Oct 10, 2026
85760b7
Keep request ids and launch-reject context in the logs
Adambomb210 Oct 10, 2026
a051e5f
Show a page instead of JSON when Weave is down
Adambomb210 Oct 10, 2026
ede2194
Turn off the API docs in production
Adambomb210 Oct 10, 2026
acafc1c
Add a tab icon and give the budget estimator two columns
Adambomb210 Oct 10, 2026
4a4d66a
Add a per-project hourly cost cap column
Adambomb210 Oct 10, 2026
ee04dd5
Cap each launch at its project's own hourly cap
Adambomb210 Oct 10, 2026
cc89daf
Let admins set a project's hourly cap
Adambomb210 Oct 10, 2026
96ef044
Call the pricing page's cap the default
Adambomb210 Oct 10, 2026
363d4d2
Pin SeaweedFS to 4.47, the version staging ran
Adambomb210 Oct 10, 2026
b6602fa
feat: add the quilt_outbox table
jaspermayone Oct 10, 2026
4c28dd9
feat: get a weave token with the quilt scope
jaspermayone Oct 10, 2026
25cde39
feat: write quilt events in the same transaction as the change
jaspermayone Oct 10, 2026
c356ffc
feat: send the quilt outbox from the worker
jaspermayone Oct 10, 2026
0509e6a
feat: show quilt delivery on the admin page
jaspermayone Oct 10, 2026
f366fa2
docs: describe the quilt integration
jaspermayone Oct 10, 2026
328a206
Merge Jasper's SkyPilot offboarding and live Weave role tests
Adambomb210 Oct 10, 2026
2756abc
fix: follow quilt's overspend rule in the release cap
jaspermayone Oct 10, 2026
38a0fa0
Merge Jasper's Quilt outbox (patchworklabsorg/krater#4)
Adambomb210 Oct 10, 2026
cce29aa
Show the Quilt section on /admin once, outside the page title
Adambomb210 Oct 10, 2026
4136b9f
Send X-Auth-Request headers from the SkyPilot sign-in proxy
Adambomb210 Oct 11, 2026
2f0549b
Describe the Code of Conduct and sky login steps in the staging runbook
Adambomb210 Oct 11, 2026
a33d8c8
Add an off compute mode so production can run without SkyPilot
Adambomb210 Oct 11, 2026
b67b837
Mark the SkyPilot-only steps in the production runbook
Adambomb210 Oct 11, 2026
faf2804
feat: redesign the web ui and fix the page titles (#6)
jaspermayone Oct 11, 2026
8a06040
Merge Jasper's UI redesign and page titles (patchworklabsorg/krater#6)
Adambomb210 Oct 11, 2026
d183964
Merge claude/quilt-merge: Jasper's UI redesign (patchworklabsorg/krat…
Adambomb210 Oct 11, 2026
c987349
Stop storing copies of member data from Weave
Adambomb210 Oct 12, 2026
4d0ce7c
Move what outlives SkyPilot out of it, and rename its settings
Adambomb210 Oct 11, 2026
eb7f477
Remove SkyPilot
Adambomb210 Oct 11, 2026
29ff00e
Describe Krater without SkyPilot in the docs
Adambomb210 Oct 12, 2026
e2f7eb0
Name the project page's hint and banner styles after what they show
Adambomb210 Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Keep the host's virtualenv out of the build context: `COPY . .` would otherwise merge it into the image's
# own /app/.venv (on Windows that overwrites its pyvenv.cfg with a C:\ interpreter path).
.venv/
venv/
# Never bake local env files (live secrets) into an image.
.env
.env.*
!.env.example
.weave_e2e_fixture.json
.git/
**/__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
*.log
backups/
86 changes: 86 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copy to `.env` and adjust (or keep it outside the repo and set KRATER_ENV_FILE; see docs/dev/staging.md
# section 4). See krater/config.py for the full list of settings.

KRATER_ENV=development
# In docker-compose, the db host is the `db` service; for `bin/dev` locally, use localhost.
KRATER_DATABASE_URL=postgresql+psycopg://krater:krater@db:5432/krater
# Generate a real one for anything beyond local dev:
# python -c "import secrets; print(secrets.token_urlsafe(48))"
# Production refuses short keys and anything containing "change-me".
KRATER_SECRET_KEY=change-me-in-production
KRATER_BASE_URL=http://localhost:8000

# Weave (Patchwork Labs identity provider). Weave signs people in and owns their Krater roles. Krater asks
# for the scopes `openid profile email groups roles slack`, and uses a client_credentials token (scope
# `directory`) of the same app for the directory API. See docs/weave-integration.md.
# `stub` replaces Weave with a local fixture of fake users; required outside of production.
KRATER_WEAVE_MODE=stub
KRATER_WEAVE_ISSUER=
KRATER_WEAVE_CLIENT_ID=
KRATER_WEAVE_CLIENT_SECRET=
# Base URL of Weave's directory API. Leave blank to use KRATER_WEAVE_ISSUER.
KRATER_WEAVE_API_BASE_URL=
KRATER_WEAVE_STUB_USERS_FILE=

# Krater's role keys on its Weave app, and the group slugs Krater falls back to when Weave sends no
# `roles` field at all. The defaults match the Weave setup in docs/weave-integration.md.
# KRATER_WEAVE_ROLE_MEMBER=member
# KRATER_WEAVE_ROLE_REVIEWER=reviewer
# KRATER_WEAVE_ROLE_ADMIN=admin
# KRATER_WEAVE_GROUP_MEMBER=ganymede-members
# KRATER_WEAVE_GROUP_REVIEWER=krater-reviewers
# KRATER_WEAVE_GROUP_ADMIN=krater-admins

# Which compute layer launches members' machines: `vast` (Krater Compute, docs/compute/) or `off`. Off by default.
KRATER_COMPUTE_MODE=off


# Slack integration (review happens in Slack). See docs/SPEC.md "Slack integration" and
# docs/dev/slack-setup.md. `fake` = in-memory Slack for local dev/tests; required outside of production.
KRATER_SLACK_MODE=fake
# Bot token (`xoxb-...`) for Krater's own Slack app -- see docs/dev/slack-setup.md for the required scopes.
KRATER_SLACK_BOT_TOKEN=
# Signing secret from the app's "Basic Information" page, used to verify requests to /slack/interactions.
KRATER_SLACK_SIGNING_SECRET=
# The master feed channel's id (e.g. `C0123456789`), for the one-line post on each new proposal.
KRATER_SLACK_FEED_CHANNEL_ID=
# How often the worker invites new reviewers (anyone granted ganymede:reviewer in Krater), posts missed
# budget warnings, and archives channels of projects that were missed.
KRATER_SLACK_RECONCILE_INTERVAL_MINUTES=10

# S3-compatible object storage for gallery screenshots. See docs/dev/storage.md.
# docker-compose runs a temporary SeaweedFS container as `storage`. The app talks to it on the internal endpoint;
# browsers get presigned URLs on the public one. Change the secret for anything beyond local dev.
# `live` uses that container; `fake` is in-memory and hands browsers unusable `fake://` upload URLs.
KRATER_S3_MODE=live
KRATER_S3_ENDPOINT_URL=http://storage:8333
KRATER_S3_PUBLIC_ENDPOINT_URL=http://localhost:8333
KRATER_S3_REGION=us-east-1
KRATER_S3_BUCKET=krater-screenshots
KRATER_S3_ACCESS_KEY_ID=krater
KRATER_S3_SECRET_ACCESS_KEY=krater-dev-secret

# docker-compose only: the `db` service's own credentials.
POSTGRES_USER=krater
POSTGRES_PASSWORD=krater
POSTGRES_DB=krater
PORT=8000

# Production only (docs/dev/production.md). Leave unset for dev and staging.
# Set to 127.0.0.1 so published ports are reachable only from the host, behind the TLS proxy.
# KRATER_BIND_ADDRESS=127.0.0.1
# Number of reverse proxies in front of the portal (1 with the `proxy` profile), for rate limiting.
# KRATER_TRUSTED_PROXY_COUNT=1
# SeaweedFS version; defaults to 4.47, the one staging ran. Change it only after a staging run, never to `latest`.
# SEAWEEDFS_VERSION=4.47
# `--profile proxy`: the public hostnames Caddy gets certificates for.
# KRATER_DOMAIN=krater.example.org
# KRATER_S3_DOMAIN=storage.krater.example.org
# `--profile backup`: where nightly database dumps go, how often, and how long they're kept.
# KRATER_BACKUP_DIR=/srv/krater/backups
# KRATER_BACKUP_INTERVAL_SECONDS=86400
# KRATER_BACKUP_KEEP_DAYS=14

# The public GPU price catalog behind /pricing and the budget estimator (docs/dev/pricing.md). `live` fetches it
# from GitHub; production refuses `fake`. KRATER_SKYPILOT_MODE still works as the old name for one release.
KRATER_PRICING_MODE=fake
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Shell scripts must keep LF endings even in a Windows checkout (core.autocrlf=true): WSL and Linux
# bash reject CRLF, and some dev scripts run in WSL2.
*.sh text eol=lf

# Self-hosted fonts are binary; never let autocrlf touch them.
*.woff2 binary
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: krater_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U root"
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
KRATER_ENV: test
KRATER_DATABASE_URL: postgresql+psycopg://root:root@localhost:5432/krater_test
KRATER_TEST_DATABASE_URL: postgresql+psycopg://root:root@localhost:5432/krater_test
KRATER_SECRET_KEY: ci-test-secret

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.8.17"
enable-cache: true

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync --frozen

- name: Lint (ruff check)
run: uv run ruff check .

- name: Format check (ruff format)
run: uv run ruff format --check .

- name: Run tests
run: uv run pytest
40 changes: 40 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Deploy

# Deploys a commit of this repo to alastor. There's no registry image: the NixOS module
# (patchworklabsorg/infra, modules/krater) builds the image on alastor from the commit written to
# /var/lib/krater/source-rev. This workflow writes that file, builds while the running site keeps
# serving, then restarts krater-web, which migrates and starts; krater-worker follows it. Manual only,
# like Weave's production deploys. See docs/dev/production.md.
#
# Required repository secrets:
# ALASTOR_HOST - alastor's SSH host / IP
# KRATER_DEPLOY_SSH_KEY - private key for the krater-deploy user on alastor (its public key goes in
# the infra repo's hosts/alastor/krater.nix, deployAuthorizedKeys)

on:
workflow_dispatch:

concurrency:
group: deploy-krater
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Deploy ${{ github.sha }}
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.ALASTOR_HOST }}
username: krater-deploy
key: ${{ secrets.KRATER_DEPLOY_SSH_KEY }}
# The first build of the Python dependencies takes several minutes on alastor.
command_timeout: 50m
script: |
set -e
echo "${{ github.sha }}" > /var/lib/krater/source-rev
sudo systemctl start krater-build
sudo systemctl restart krater-web
systemctl is-active krater-web krater-worker
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.eggs/

# Virtual environments
.venv/
venv/

# uv
# (uv.lock IS committed -- do not ignore it)

# Environment / secrets
.env
.env.*
!.env.example
# Live-Weave e2e fixture (scripts/dev/weave_e2e_setup.py) -- carries a live OAuth client secret
# and service API key, regenerated on every run. See docs/dev/weave-e2e.md.
.weave_e2e_fixture.json

# Testing / tooling
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/

# Editors / OS
.vscode/
.idea/
.DS_Store

# Logs
*.log

# Database dumps from the db-backup service (docker-compose.yml, --profile backup).
/backups/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
90 changes: 90 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# CLAUDE.md

Guidance for Claude Code (and humans) working in this repo.

## What this is

Krater is the Project Ganymede portal (Patchwork Labs): proposal review, compute budget allocation/enforcement, and a
public gallery. **Read `docs/SPEC.md` before changing behavior.** Integration contracts live in
`docs/weave-integration.md` (sign-in and roles) and
`docs/quilt-integration.md` (submission and budget events to Quilt).

## Stack

- Python 3.12, managed with **uv** (`pyproject.toml` + `uv.lock`)
- **FastAPI** web app, server-rendered **Jinja2** templates (no SPA; progressive enhancement with HTMX is fine)
- **SQLAlchemy 2.0** (typed `Mapped[...]` models, sync sessions, psycopg 3) + **Alembic** migrations
- **Postgres** (16+)
- **procrastinate** (Postgres-backed) for background jobs and periodic tasks — no Redis
- **Authlib** for OIDC against Weave; Starlette `SessionMiddleware` for the session cookie
- **pytest** against a real Postgres test database; **ruff** for lint + format

## Commands

```bash
uv sync # install deps (incl. dev)
uv run alembic upgrade head # migrate (DATABASE_URL must be set)
uv run alembic revision --autogenerate -m "..." # new migration; always review the output
uv run uvicorn krater.web.app:create_app --factory --reload # web
uv run procrastinate --app=krater.worker.app.app worker # worker
uv run pytest # tests (TEST_DATABASE_URL, see tests/conftest.py)
uv run ruff check . && uv run ruff format --check .
docker compose up --build # full stack: portal, worker, db
```

## Layout

```
krater/
config.py Settings (pydantic-settings, env vars prefixed KRATER_)
db.py engine, session factory, Base
models/ SQLAlchemy models, one module per aggregate
services/ domain logic (framework-free): approval_policy, projects, budget, audit, users
weave/ WeaveClient protocol: OIDC sign-in, directory API, role mapping, dev stub
quilt/ Quilt patch API client and the outbox sender (the only code that knows Quilt's URLs)
web/ FastAPI app factory, routers, deps (current user, authz), templates/, static/
worker/ procrastinate app + tasks
alembic/ migrations
tests/ mirrors krater/ layout
```

## Conventions

- **Services own the rules.** Routers parse input, call a service, render. Services take a `Session` plus an explicit
actor and raise domain errors (`krater.services.errors`); they never import FastAPI.
- **Money is integer cents** (`*_cents` columns, `int` in Python). Never floats.
- **Primary keys are UUIDs** (`uuid.uuid4`), safe to show in URLs.
- **Weave owns roles.** Krater's role keys in Weave are `member`, `reviewer` and `admin`, sent in the `roles` claim
and in directory records. Only when the `roles` field is absent does Krater fall back to the group slugs
`ganymede-members`, `krater-reviewers` and `krater-admins`. `krater.weave.roles` is the one place that maps them to
Krater's internal names `ganymede:member`, `ganymede:reviewer`, `ganymede:admin`; nothing else knows the keys or slugs.
Every state-changing action (`fresh_actor`, the Slack click path) re-checks Weave's directory by `sub` through
`krater.services.users.authorize`, and fails closed if Weave is down. `users.roles_cached` is for display only.
Krater has no role tables, no admin users page and no disable switch: those are done in Weave.
- **Weave owns member data.** Krater stores a member's Patchwork ID (`users.weave_sub`), never their name, email or
other personal details: read those from Weave when needed (`krater.services.people`) and record who acted by
Patchwork ID (`docs/weave-integration.md` "Member data").
- **All Weave access goes through `krater.weave`.** Nothing else imports httpx for Weave or knows Weave's URLs.
Krater asks for `openid profile email groups roles slack`, and calls the directory with a client_credentials token
of its own app (scope `directory`). Its token for Quilt (scope `quilt`) comes from `WeaveClient.quilt_token`.
- **Events for Quilt go through the outbox.** A service that changes a project's status, title, ledger or spend
calls `krater.services.quilt_events.sync_project` in the same transaction. Never post to Quilt from a request.
- **Every admin override writes an `AuditEvent`** (who, what, reason). Budget changes write a `BudgetEntry` (append-only;
never update or delete ledger rows).
- **Stub mode** (`KRATER_WEAVE_MODE=stub`) replaces Weave with a local fixture of fake users for dev and tests. The
fixture carries the same `roles`, `groups` and `slack_id` fields a real Weave sends, and the stub answers the
directory lookups too. Tests change a user mid-test with `StubWeaveClient.set_roles` and friends. The app must
refuse to start in stub mode when `KRATER_ENV=production`.
- Timestamps are timezone-aware UTC (`DateTime(timezone=True)`).
- Tests: every service rule gets a unit test; every route gets at least a happy-path and an authz test.
- Keep comments for the *why*; match the surrounding style.

## Brand

Krater uses the Patchwork palette with the same role tokens as Quilt's stylesheet: Ink `#2a2050` text on Quilt
`#f7f5fb`, Grape `#6c3ec1` as the primary accent, Deep Purple `#4b006e` for hover, Lavender `#c7b7e2` in the brand
mark, solid hairline cards, Geist for all text, and a dark mode. The tokens live on `:root` in
`krater/web/static/css/main.css`; components use the role tokens (`--surface`, `--text`, `--accent` ...), never raw
colors. Style through the existing classes (`.page-head`, `.card`, `.stats`, `.button--primary`, `.badge--<status>`,
`.table-scroll` ...). Pages give only their own name in `{% block title %}`; `base.html` adds " · Krater". Every
page must work at 360px wide with no horizontal page scroll.
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
#
# Builds for both amd64 and arm64 (e.g. `docker buildx build --platform linux/amd64,linux/arm64 .`):
# both `python:3.12-slim` and the `uv` image below publish multi-arch manifests, and nothing here
# pins an architecture.
FROM python:3.12-slim

# Copy the `uv` and `uvx` binaries from Astral's official image rather than installing via pip.
COPY --from=ghcr.io/astral-sh/uv:0.8.17 /uv /uvx /usr/local/bin/

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"

WORKDIR /app

# Install dependencies before copying the rest of the source, so `docker build` caches this layer as
# long as pyproject.toml / uv.lock haven't changed.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

RUN groupadd --system krater \
&& useradd --system --gid krater --create-home --home-dir /home/krater krater \
&& chown -R krater:krater /app
USER krater

EXPOSE 8000

# The portal is the default; docker-compose.yml overrides `command` for `migrate` and `worker`.
CMD ["uvicorn", "krater.web.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"]
Loading
Loading