Deploying Loreweaver
Contents
Most tables are hosted peer-to-peer from a laptop — just python -m app --serve, or one-click
Host locally from the connect screen (see the README). This page is for running
an always-on server (a 24/7 public game with a stable ticket). Loreweaver connects over Iroh
— p2p QUIC, dialed by a ticket, with no domain, TLS, port-forward, or reverse proxy. Players
join with deployer-issued keys; there is no account system. (There is no Docker image or
WebSocket player-client serve path — the old TUI WebSocket remains only for offline tests.)
Run it (bare metal)
Requires Python ≥ 3.11 and uv.
git clone https://github.com/1A7432/loreweaver && cd loreweaver
cp .env.example .env # then set TRPG_LLM__* (or leave blank for the offline demo)
uv sync # env + deps (Iroh is a default dep)
uv run python -m app --serve --keys ./data/keys.toml
On first run the server auto-mints a keeper key and prints a shareable Iroh ticket — both
are also written next to the keystore as keeper-key.txt / iroh-ticket.txt. Share the ticket +
the keeper key; connect with them, then mint more keys / create rooms right in the client's Rooms
& invites screen — no server access needed. State (SQLite + keys) lives next to --keys.
Behind a SOCKS proxy for a non-China LLM?
uv pip install socksio. A China-direct provider (e.g. DeepSeek) needs no proxy — run with a clean env.
Keep it running (systemd)
# /etc/systemd/system/loreweaver.service — replace YOU with your username
[Unit]
Description=Loreweaver Iroh server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=YOU
WorkingDirectory=/home/YOU/loreweaver # .env is loaded from here
ExecStart=/home/YOU/.local/bin/uv run python -m app --serve --keys /home/YOU/loreweaver-data/keys.toml
Restart=on-failure
RestartSec=10
TimeoutStartSec=120 # Iroh's relay handshake takes a moment
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now loreweaver
journalctl -u loreweaver -f # follow logs — the ticket + keeper key print at startup
Updating
A keeper updates the server from the client's Rooms & invites page — no SSH needed.
It is on by default for the git-checkout deployment above: a keeper (only) sees the
server-vs-client version and an Update server button whenever the client is newer.
Pressing it runs TRPG_TUI__UPDATE_COMMAND on the server (default
git pull --ff-only && uv sync), then the process re-execs itself into the new code
(same PID, so the Iroh ticket is unchanged and no systemctl restart is needed — it works
with the Restart=on-failure unit above). Clients briefly disconnect and reconnect.
loreweaver update does the same server update by default after reinstalling the client
(using your saved keeper connection), so one command keeps both in step —
loreweaver update --client-only skips the server.
Override the command for a non-git deployment, or disable the feature entirely:
# in ~/loreweaver/.env
TRPG_TUI__UPDATE_COMMAND=git pull --ff-only && uv sync # default (git checkout)
# TRPG_TUI__UPDATE_COMMAND=docker compose pull && docker compose up -d # your own mechanism
# TRPG_TUI__UPDATE_COMMAND= # blank → button hidden, feature off
You can still update by hand any time:
cd ~/loreweaver && git pull && uv sync && sudo systemctl restart loreweaver
Security: the command is yours, from server-side config — a client can only ask the server
to run its own configured command, never supply one. The default only pulls from the
checkout's own git remote. The keeper key is the trust boundary, exactly as for
.model/key management; set TRPG_TUI__UPDATE_COMMAND blank if you'd rather it never run.
Configuration
All settings use the TRPG_ env prefix with __ for nesting (see
.env.example / infra/config.py), loaded from .env in the working directory unless
TRPG_ENV_FILE points at a different file. The TUI's one-click local host path sets
TRPG_ENV_FILE=<local server folder>/.env automatically.
| Variable | Purpose | Default |
|---|---|---|
TRPG_LLM__PROVIDER |
openai (+ presets: deepseek, groq, openrouter, together, ollama, lmstudio, …), dual-mode chatgpt / gpt-subscription, subscription supergrok, or native anthropic / gemini |
openai |
TRPG_LLM__API_KEY |
provider/proxy API key — not used by a subscription OAuth path; blank = offline demo Keeper for normal API-key providers | (empty) |
TRPG_LLM__BASE_URL |
OpenAI-compatible base URL; an explicit value selects the proxy path for chatgpt / gpt-subscription, while blank selects subscription OAuth |
provider preset |
TRPG_LLM__CHAT_MODEL |
chat model id | gpt-4o |
TRPG_LLM__EMBEDDING_MODEL / TRPG_LLM__EMBEDDING_DIM |
retrieval embeddings | text-embedding-3-small / 1536 |
TRPG_LOCALE |
UI language en / zh |
en |
TRPG_ENV_FILE |
explicit .env file to load before starting the server |
.env in the working directory |
TRPG_DATA_DIR |
campaign/runtime data directory (db → <data_dir>/loreweaver.db) |
./data |
TRPG_TUI_KEYS |
keystore file path (also overridable with --keys; independent of TRPG_DATA_DIR) |
./keys.toml |
TRPG_LOCAL_SERVER_HOME |
TUI one-click local hosting root: server binary/source cache, .env, data, keys, and ticket sidecars |
TRPG_HOME, else <user home>/.loreweaver |
TRPG_RELEASE_TAG |
Pin the installer/client and one-click server downloads to a versioned GitHub Release such as release-0.5.1.dev29+g0cf542b |
latest release |
TRPG_SERVER_RELEASE_TAG |
Pin only the one-click server binary/source download tag; the installer writes this automatically for release builds | TRPG_RELEASE_TAG, else latest release |
TRPG_ENABLE_VECTOR_DB |
worldbook / document retrieval | true |
TRPG_TUI__JOIN_TIMEOUT |
seconds an unauthenticated connection has to send join before being closed |
10 |
TRPG_CENSOR__WORDLIST_PATH |
Content-moderation wordlist: a JSON file {"word": level, ...} (level 1-5, see gateway.ops.CensorLevel). See Content moderation |
(empty = moderation OFF) |
TRPG_CENSOR__WORDLIST |
Content-moderation wordlist, inline: word[:level],word2[:level2],... — an alternative to a file, handy for one env var. Combines with WORDLIST_PATH if both are set |
(empty = moderation OFF) |
ChatGPT subscriptions are not API keys. For the direct subscription path, start
the server, run .model login chatgpt from a private/local Keeper chat, complete
the device-code flow, then run .model set chatgpt [model]. Leave
TRPG_LLM__BASE_URL blank for this path; Loreweaver uses the saved OAuth grant,
not browser cookies or web-session automation. .model login supergrok followed
by .model set supergrok [model] selects the SuperGrok subscription path and can
also supply its image-generation bearer.
Existing compatible gateways remain supported: set provider to chatgpt or
gpt-subscription, explicitly set TRPG_LLM__BASE_URL=<gateway /v1 endpoint>,
and provide the gateway API key. An explicit base_url always selects this
classic proxy path rather than subscription OAuth.
Release builds publish an adjacent .sha256 for every client and server archive. The
installer verifies the client digest before extraction; one-click hosting verifies the
selected server archive. If an older binary release has no sidecar, or its checksum metadata is
unreachable or malformed, one-click hosting falls back to the selected source path. A valid
checksum that does not match the downloaded archive is fatal. Untagged main builds and numeric
stable v* tags become GitHub's Latest release; explicitly tagged prereleases remain
pre-releases. The HTTP mirror keeps a flat compatibility copy for its one-line installer and an
immutable copy at releases/<tag>/ for every published build. A released or pinned installer
uses the tag-specific mirror copy, so a later development publish cannot replace its fallback
archive. An embedded digest is accepted only when the selected tag matches the installer's
embedded tag; other selections fetch the selected archive's sidecar. Checksum mismatches are
fatal and never trigger extraction or a fallback to a different payload.
The installer uses https://registry.npmjs.org by default; set TRPG_REGISTRY only when you
intentionally choose another registry.
Encryption
Iroh player connections are end-to-end encrypted, with nothing to configure (QUIC/TLS, each peer authenticated by its public key), with no certificate to manage. This protects traffic between an OpenTUI player and the Loreweaver server; it does not say what the server sends to a configured model provider.
Data flow and trust boundaries
- The deterministic rules engine, SQLite campaign state, media, room keys, and backups stay on the server you operate. The Iroh relay, when one is needed, carries encrypted traffic and does not terminate the application session.
- A remote LLM endpoint is a separate data processor. It receives module text for analysis, the Keeper system prompt (which by design carries the module's Keeper-only lore), relevant conversation history, and the current player input. The standard app uses a local hash embedder; if an embedding backend is explicitly replaced with a remote implementation, document chunks also go to that endpoint. Select a local endpoint such as Ollama or LM Studio when this material must remain on infrastructure you control.
- The player knowledge pool and every NPC or companion are kept apart by the code itself: a sub-actor is built only from its own record and sheet. The main Keeper is intentionally different — it sees secrets in order to run the mystery. Prompt instructions and the nightly live-model red-line eval reduce and measure its leak risk; they are not a proof that every model will behave.
- Player keys are room-scoped. Keeper keys can read Keeper-only state and manage keys only for their own room, but provider/model configuration is deployment-wide. Issue Keeper keys only to fully trusted co-administrators. A caller-supplied custom provider URL is never paired with an older saved API key unless the caller supplies that key for the new endpoint.
- Provider API keys and subscription OAuth grants are stored unencrypted in the local SQLite database so hot configuration survives restart. They are sent as authorization only to the chosen provider endpoint, not to players. Treat the host account and its backups as part of the trusted computing base.
Content moderation
gateway.ops.Censor is a real, bypass-resistant word matcher (NFKC + casefold
normalization, de-obfuscation for spaced/punctuated/fullwidth spellings,
whole-word boundaries, offset-preserving masking) — but it ships with no
wordlist and is OFF by default. Loreweaver deliberately does not bundle a
profanity/slur list: maintaining one, and getting multilingual coverage
right, is a policy choice each deployer should own, not something baked into
the engine. With no wordlist configured, Censor takes an explicit no-op
path on every call — it is not silently filtering anything.
To turn it on, set one of TRPG_CENSOR__WORDLIST_PATH (a JSON file) or
TRPG_CENSOR__WORDLIST (an inline list) — see the
Configuration table above. Example file:
{ "some-slur": 5, "some-mild-word": 2 }
Levels are 1 (NOTICE) through 5 (FORBIDDEN); a hit at DANGER (4)
or above blocks the message (the reply is replaced), below that it is masked
in place. Word matching is locale-agnostic — list whatever words/scripts you
need moderated.
Current scope — read before relying on this:
- It only screens the AI Keeper's own narration (
agent.loop.run_kp_turn'soutput_review, wired ingateway.runner.GatewayRunnerandnet.tui_server.TuiServer). Player input is not screened. A player can type anything; only what the Keeper says back is checked. - It is a wordlist matcher, not a semantic classifier — it catches listed words (and simple obfuscations of them), nothing it wasn't told about.
Do not treat this as a moderation solution out of the box — it is a configurable building block that does nothing until you supply a wordlist.
Keys & persistence
- Keys bind an opaque token to a
room(the sharedchat_key) and a role. Mint with--tui-key add; unknown keys are rejected on join. The keystore is a TOML file (keys.toml) — never commit it. - Persistence is a single SQLite file (
loreweaver.db) holding all campaign state, scoped byroom. Keep the/datavolume to keep progress. - Provider credentials entered at runtime, including subscription OAuth
access/refresh grants, are stored unencrypted in that local SQLite file so
they survive restart. Protect the database like
.envorkeys.toml. - Room backups created from the keeper admin UI are server-side JSON
snapshots confined to
<data_dir>/room_backups/; an optional path is treated as a filename inside that directory. They include raw access keys, room state, vector data, and self-contained media blobs, so protect them likekeys.toml. - Local permissions are tightened on new secret-bearing files (
0600) and dedicated data / backup directories (0700) where the filesystem implements POSIX modes. On Windows or filesystems without POSIX permissions this is best-effort, not an ACL manager. - Secrets (
.env,keys.toml,keeper-key.txt,*.db, backups) are git-ignored; only*.example.*files are tracked. Never commit them.
Connecting clients
Clients speak the versioned protocol in docs/protocol.md over Iroh. Point the
terminal client at the server's ticket (printed at startup) with a minted key:
cd clients/tui && bun install
bun run dev -- connect --host <ticket> --key <key> --name <name>
# or just `loreweaver` (installed client) and paste the ticket + key in the connect screen
The connection is end-to-end encrypted; the server is key-gated, but treat keys as secrets.