M2: cut the hosted product away from the local one
94 files, +1,395 -6,578. Three files are new; twenty-four are gone. The milestone is subtraction, and what is left is the single-user local storyteller the specification describes. Removed in full: campaign scripting and its QuickJS sandbox; multi-user accounts, guest sessions, login, registration and the shared demo key; the visitor-analytics tables, dashboard and page beacon; the access log of sign-ins, addresses and devices; per-IP and per-user rate limiting and quotas; Render deployment config; Postgres and psycopg; cloud inference providers, the API-key field and the key encryption that existed to store it; session-cookie signing. None of it was hidden behind a flag — the routes are gone and answer 404. Two things were kept that the brief allowed keeping. The `users` table and its foreign keys stay as an internal ownership detail, because rewriting them out means a migration across most of the schema to delete a column that costs nothing; nothing creates a second user and no request carries an identity. Five inert tables and four inert columns stay for the same reason, so an M1 campaign database opens unchanged. The one addition is app/endpoints.py, which decides where a story may be sent. Loopback, RFC1918, link-local, unique-local and CGNAT — an explicit allowlist of networks, not a guess at what `ipaddress` means by "private", which calls the documentation ranges private and IPv6 loopback reserved. Every address a hostname resolves to must be in it, so a split answer does not squeak through, and the rule runs both when the endpoint is saved and before every outbound request, because a name that resolved to the LAN this morning can resolve elsewhere this afternoon. Known cloud hosts are named in the refusal so the error says why rather than looking like broken DNS. TLS is never traded against it: M1's shared trust context is intact on all four clients and there is no way to skip verification. The hardcoded 120-second model timeout is now a setting. That was not theoretical — on this GPU-less four-core host a cold load of qwen2.5:3b-instruct took 648.9 seconds to produce the first turn, while turns 2 to 5 of the same campaign took 3.6 to 13.1. Connect stays short at 10s so a wrong address still fails fast; the read timeout defaults to 300s and is bounded at 3600, because "wait longer" must stay a number. Two defects found while testing and fixed here. An unknown /api path fell through the SPA catch-all and came back as HTML with status 200, so a client asking for JSON parsed a web page instead of learning the route was gone. And AIDND_CORS_ORIGINS accepted "*", which on an unauthenticated loopback API would hand every page on the Internet a write handle on the campaign database; it now refuses to start. Verified rather than assumed. Offline, on a network with no route out and no DNS: five turns, retry with both takes retained, restart with an identical transcript digest, a failed model call leaving the accepted AI-turn count untouched, and a capture with zero non-loopback unicast packets. Against a real second machine on the LAN over HTTPS with a private CA: four turns, restart, and a capture showing 289 packets to the approved host, 344 loopback, zero anywhere else, zero DNS queries. Cloud and public endpoints refused with their reasons; no API key settable; every removed route 404. 604 backend tests pass, down from 648 by the fifteen retired with the subsystems they tested and up by the twenty-nine added for the endpoint policy and the removed surface. The scripting tests were not deleted: eight files used a JavaScript counter as instrumentation for the state snapshot and rollback machinery, which M2 does not touch, so the counter moved to the world-state engine and those tests still assert what they always did. Frontend lint and build are clean; the image builds, and its wheel-building stage is gone with quickjs. No M3 work. Undo is still destructive and there is still no Redo.
This commit is contained in:
+14
-100
@@ -1,110 +1,24 @@
|
||||
# Environment variables read by the backend.
|
||||
#
|
||||
# NOTE: the app reads real environment variables — it does NOT auto-load this
|
||||
# file. Set them in your shell, in docker-compose.yml, or in your host's
|
||||
# dashboard. This file is documentation (and a template for deploy configs).
|
||||
# file. Set them in your shell or in docker-compose.yml. This file is
|
||||
# documentation.
|
||||
#
|
||||
# There are two, and neither is required. Everything about the model — the
|
||||
# endpoint, the model names, the timeout, the context budget — is a runtime
|
||||
# setting stored in the database and edited on the Settings page, because it is
|
||||
# a preference rather than a deployment detail.
|
||||
|
||||
# Absolute path for the SQLite database file. Parent directory is created if
|
||||
# missing. Default when unset: backend/data.db
|
||||
# Docker compose sets this to /data/data.db (a named volume).
|
||||
# Absolute path for the SQLite database file. The parent directory is created
|
||||
# if missing. Default when unset: backend/data.db
|
||||
# docker-compose.yml sets this to /data/data.db (a named volume).
|
||||
AIDND_DB_PATH=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 9 — production hardening
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Switch from SQLite to a server database (hosted deploys use Neon Postgres).
|
||||
# Any SQLAlchemy URL; postgres:// and postgresql:// schemes are rewritten to
|
||||
# the psycopg3 driver automatically. The platform-conventional DATABASE_URL
|
||||
# is honored too (AIDND_DATABASE_URL wins if both are set). Unset = SQLite.
|
||||
AIDND_DATABASE_URL=
|
||||
|
||||
# Comma-separated list of allowed CORS origins. Only needed when the frontend
|
||||
# is served from a different origin than the API; the production build is
|
||||
# served same-origin by FastAPI, so hosted deploys can leave this unset.
|
||||
# served same-origin by FastAPI, so a normal run can leave this unset.
|
||||
# Default: http://localhost:5173,http://127.0.0.1:5173 (the Vite dev server).
|
||||
#
|
||||
# A wildcard is rejected. The storyteller API is unauthenticated by design and
|
||||
# bound to loopback; letting any origin call it would undo that.
|
||||
AIDND_CORS_ORIGINS=
|
||||
|
||||
# How many proxy hops the rate limiter trusts in `X-Forwarded-For`. It reads
|
||||
# the entry that many places from the right, because the trusted edge appends
|
||||
# the real client IP last. Set this to the number of proxies in front of the
|
||||
# app. Default: 1, which is correct for a single edge such as Render.
|
||||
#
|
||||
# Get it wrong in either direction and the rate limits weaken. Too low reads an
|
||||
# entry the caller supplied, so anyone can rotate the header for a fresh
|
||||
# rate-limit bucket per request and walk past the auth and guest limits. Too
|
||||
# high reads past the real client. Only multi-user mode rate-limits at all, so
|
||||
# local installs can ignore this.
|
||||
AIDND_TRUSTED_PROXY_HOPS=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 8 — optional accounts & multi-user (all optional; defaults keep the
|
||||
# app in frictionless single-user "local mode")
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# "1"/"true" turns on multi-user mode: guest sessions via signed cookies,
|
||||
# register/login UI, per-user data. Leave unset for local installs.
|
||||
AIDND_MULTI_USER=
|
||||
|
||||
# Secret for signing session cookies and encrypting stored API keys at rest.
|
||||
# If unset in local mode, one is auto-generated into `secret.key` next to the
|
||||
# database (fine for local/docker-volume runs). REQUIRED when
|
||||
# AIDND_MULTI_USER is on — the app refuses to start without it, because a
|
||||
# regenerated secret on an ephemeral hosted filesystem would log out every
|
||||
# user on each deploy. Generate one:
|
||||
# python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
AIDND_SECRET_KEY=
|
||||
|
||||
# Session cookie Secure flag (HTTPS-only). Defaults to on when
|
||||
# AIDND_MULTI_USER is on, off otherwise — set 0/1 only to override (e.g. 0
|
||||
# when testing multi-user mode over plain http on a LAN address).
|
||||
AIDND_COOKIE_SECURE=
|
||||
|
||||
# --- Shared demo key (BYOK fallback; only active when AIDND_MULTI_USER=1) ---
|
||||
# Users with no API key of their own get this server-funded endpoint with a
|
||||
# model whitelist and a per-day turn cap. Unset = no demo, users must bring
|
||||
# their own key. Memory bank/auto-summarization are disabled on demo turns.
|
||||
AIDND_DEMO_API_KEY=
|
||||
# Default endpoint if unset: https://openrouter.ai/api/v1
|
||||
AIDND_DEMO_ENDPOINT_URL=
|
||||
# Comma-separated model whitelist. Default: google/gemma-4-26b-a4b-it:free
|
||||
AIDND_DEMO_MODELS=
|
||||
# Successful AI turns per user per day on the demo key. Default: 20
|
||||
AIDND_DEMO_TURNS_PER_DAY=
|
||||
# Comma-separated emails of "power users" (trusted testers) who bypass the daily
|
||||
# demo cap entirely — unmetered turns on the shared demo key — and get the AI Chat
|
||||
# page (a plain scratchpad for talking to a model, hidden from everyone else).
|
||||
# Registered accounts only (guests have no email). Matched case-insensitively.
|
||||
# Local (single-user) installs are always treated as power users.
|
||||
AIDND_POWER_USERS=
|
||||
|
||||
# --- Visit analytics ---
|
||||
# Comma-separated emails allowed to see the Visitors dashboard (/analytics) and
|
||||
# its nav link. Deliberately separate from AIDND_POWER_USERS: a trusted tester
|
||||
# gets unmetered turns, which is no reason to hand them the traffic numbers.
|
||||
# Unset = nobody sees it in a hosted deploy. Local installs always can, and are
|
||||
# the only mode where the viewer's own visits are still counted (excluding them
|
||||
# would leave the page permanently empty on the machine it's developed on).
|
||||
# Collection itself is always on; only the dashboard is gated.
|
||||
AIDND_ANALYTICS_EMAILS=
|
||||
# Days to keep the one-row-per-visitor-per-day table that makes the funnel
|
||||
# count people rather than clicks. The daily counters are aggregate and kept
|
||||
# forever. Default: 400. Set 0 to keep visitor-days forever.
|
||||
AIDND_ANALYTICS_RETENTION_DAYS=
|
||||
|
||||
# --- Guest retention (only active when AIDND_MULTI_USER=1) ---
|
||||
# Every first visit mints a guest account, so a public demo collects one row
|
||||
# per visitor. A guest with no activity for this many days is deleted along
|
||||
# with its scenarios, adventures and actions. Registered accounts are never
|
||||
# touched. Default: 5. Set 0 to keep guests forever.
|
||||
AIDND_GUEST_RETENTION_DAYS=
|
||||
# How often a running process re-checks. The sweep also runs once at startup,
|
||||
# which is what actually fires on hosts that sleep. Default: 6
|
||||
AIDND_CLEANUP_INTERVAL_HOURS=
|
||||
|
||||
# The AI endpoint/API key/model are NOT env vars — they are configured at
|
||||
# runtime in the app's Settings page and stored (encrypted) in the database.
|
||||
#
|
||||
# Rate limits, request size limits, and per-user row caps are hardcoded with
|
||||
# generous values (see backend/app/limits.py) and active only in multi-user
|
||||
# mode — local installs are never throttled.
|
||||
|
||||
Reference in New Issue
Block a user