diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9dda9a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# ───────────────────────────────────────────── +# Godot 4.7 (client/) +# ───────────────────────────────────────────── +# Editor + import cache (4.x replaced .import/ with .godot/) +.godot/ +# Exported builds +/client/build/ +export.cfg +# export_presets.cfg holds keystore paths / signing config — keep secrets out of history +export_presets.cfg +# Mono/C# (only if we drop to C# per charter §16) +.mono/ +data_*/ +*.mono/ + +# ───────────────────────────────────────────── +# Python / FastAPI (api/) +# ───────────────────────────────────────────── +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +.eggs/ +build/ +dist/ +# Virtual envs +.venv/ +venv/ +env/ +ENV/ +# Tooling caches +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +.tox/ + +# ───────────────────────────────────────────── +# Secrets — the API key lives here in dev, NEVER in the client (charter §4) +# ───────────────────────────────────────────── +.env +.env.* +!.env.example + +# ───────────────────────────────────────────── +# Editors / OS +# ───────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +.DS_Store +Thumbs.db + +# ───────────────────────────────────────────── +# Logs (charter §4/§10 logs go to a store, not the repo) +# ───────────────────────────────────────────── +*.log diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..6dc1728 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,16 @@ +# Keep the build context small and secrets out of the image. +.env +.env.* +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +docs/ +README.md +Dockerfile +.dockerignore diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..c7df8f0 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,11 @@ +# Copy to .env for local dev. NEVER commit the real .env. The client never sees +# any of these — keys live only in the proxy (charter §4). + +# Where the proxy sends model calls in dev (Ollama on the homelab). +OLLAMA_BASE_URL=http://localhost:11434 + +# Prod model provider (charter §4). Leave blank in dev. +REPLICATE_API_TOKEN= + +# Port the proxy binds. compose / fly.io override this. +PORT=8000 diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..2eb6545 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,26 @@ +# /api — FastAPI proxy (charter §4). Docker-based for parity across +# dev / homelab VPS / fly.io. +FROM python:3.12-slim + +# Faster, quieter Python in containers. +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +# Install deps first so the layer caches when only app code changes. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# App code. +COPY app ./app + +# Run as non-root. +RUN useradd --create-home --uid 1000 appuser +USER appuser + +EXPOSE 8000 + +# fly.io / compose set PORT; default to 8000 (charter localhost:8000). +CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"] diff --git a/api/app/__init__.py b/api/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/app/main.py b/api/app/main.py new file mode 100644 index 0000000..2223c21 --- /dev/null +++ b/api/app/main.py @@ -0,0 +1,47 @@ +"""FastAPI proxy entrypoint — the guarding proxy of charter §4. + +Skeleton only: a health check plus the five role endpoints as stubs so the +container runs and the client has something to point at. Real prompt routing, +model selection, logging, and auth land later — all server-side (§4, §5). +""" + +from fastapi import FastAPI + +app = FastAPI(title="coc-rpg proxy", version="0.0.1") + + +@app.get("/health") +def health() -> dict: + """Liveness probe for compose / fly.io.""" + return {"status": "ok"} + + +# ── Role endpoints (charter §4) ────────────────────────────────────────────── +# The client knows these paths and nothing about which model or prompt serves +# them. Stubs for now; each will load its prompt from api/prompts/ and route to +# the configured model. + + +@app.post("/dm/narrate") +def narrate() -> dict: + return {"detail": "not implemented"} + + +@app.post("/dm/adjudicate") +def adjudicate() -> dict: + return {"detail": "not implemented"} + + +@app.post("/dm/improvise") +def improvise() -> dict: + return {"detail": "not implemented"} + + +@app.post("/npc/speak") +def npc_speak() -> dict: + return {"detail": "not implemented"} + + +@app.post("/party/banter") +def banter() -> dict: + return {"detail": "not implemented"} diff --git a/api/fly.toml b/api/fly.toml new file mode 100644 index 0000000..60f2e0f --- /dev/null +++ b/api/fly.toml @@ -0,0 +1,21 @@ +# fly.io deploy config for the proxy (charter §4, prod). Stub — set app name and +# region, then `fly deploy` from api/. Secrets (REPLICATE_API_TOKEN) go via +# `fly secrets set`, never in this file. +app = "coc-rpg-proxy" # TODO: claim a real app name +primary_region = "ord" # TODO: pick a region + +[build] + dockerfile = "Dockerfile" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + +[[http_service.checks]] + method = "GET" + path = "/health" + interval = "15s" + timeout = "2s" diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..30ba429 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,5 @@ +# FastAPI proxy runtime deps. Pinned from first resolve; bump deliberately. +fastapi==0.139.0 +uvicorn[standard]==0.51.0 +httpx==0.28.1 # calling Ollama / Replicate +pydantic==2.13.4 # request/response contracts diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..81ec3b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +# Local dev — run the proxy in Docker with live reload. +# docker compose up --build +# Ollama runs on the homelab (charter §4), not here — point OLLAMA_BASE_URL at it. +services: + api: + build: ./api + ports: + - "8000:8000" + env_file: + - ./api/.env + environment: + PORT: 8000 + # Mount source + reload so edits are live without a rebuild. + volumes: + - ./api/app:/app/app:ro + command: > + uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload