flybrain/infra/env/example.env
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

326 lines
17 KiB
Bash

# infra/env/example.env — every knob the infra scripts read, with placeholder
# values. THIS IS A TEMPLATE, NOT A DEPLOYABLE FILE.
#
# Sourced by every infra/*.sh script (as their first argument) and, once
# 05-deploy.sh has generated /etc/fly/fly.env from it, by the systemd
# EnvironmentFile= stanzas that need it.
#
# The real env files are NOT in this repo. This repo is public and a real env
# file names the host, the container id, the LAN address, the channel and the
# `pass` entry. Keep the real ones outside the checkout and pass the path:
#
# infra/provision.sh /etc/fly/env/fly-pokemon.env
# FLY_ENV_DIR=/etc/fly/env infra/05-deploy.sh fly-pokemon.env <tarball>
#
# See infra/env/README.md. Every value below is a placeholder except the ones
# that are genuinely repo-wide decisions (stream geometry, the feature flags,
# the chat deny-list path) — diff a real env against this file after a bump to
# spot new knobs.
#
# Angle-bracket values (`<release-ctid>`, `<twitch-channel>`, …) are
# placeholders and every script will refuse them or fail loudly; empty values
# mean "unset is a valid choice for this knob".
# --- where the operator's own values live -----------------------------------
# Not read by the scripts; recorded here so the next operator knows where to
# look. The real env files, the release records, the live cpuset numbers, the
# host notes and the claim-log path live in the operator's infra repo under
# `services/flybrain/`.
# --- host-side locations (read by the scripts, not by systemd) --------------
# PVE_HOST: ssh target of the Proxmox host, used only by
# infra/bin/dev-stage-deploy.sh (which runs from the operator box). No
# default: the script refuses to guess a hostname.
PVE_HOST=<pve-host-ssh-target>
# HOST_STAGE_DIR: where host-side artifacts are staged before `pct push`
# (release tarballs, the NVIDIA .run, the VirtualGL .deb). Defaults to /root
# in the scripts, because they run as root on the host.
HOST_STAGE_DIR=
# AGENT_CLAIM_LOG: the host's serialisation file — one agent at a time, claim
# the container before touching it, release it when done (CLAUDE.md,
# infra/README.md "Claim the container ID"). Scripts that would claim a
# container SKIP the claim when this is unset rather than inventing a path.
AGENT_CLAIM_LOG=
# Offsite copy, read by infra/bin/fly-backup-stage (installed on the host by
# hand — see infra/README.md). BACKUP_HOST has no default and the script
# refuses to run without it.
BACKUP_HOST=<backup-user>@<backup-host>
BACKUP_ROOT=<backup-path>
FLY_BACKUP_DIR=
# --- container identity -----------------------------------------------------
CTID=<release-ctid>
HOSTNAME=fly-pokemon
# The convention these scripts assume is DHCP plus a router-side MAC
# reservation, not an in-guest static address: this value is what you ASK the
# router to reserve, not something set inside the container. `dhcp` is a valid
# sentinel meaning "whatever the lease gives it — check `pct list` / arp".
IP=<release-ct-ip>
ONBOOT=1
STARTUP_ORDER=4
STARTUP_UP=60
# ROLE gates infra/05-deploy.sh (refuses ROLE=release unless the source tree is
# exactly at a clean annotated vX.Y.Z tag — infra/lib/common.sh's
# require_release_tag, infra/build/tag-release.sh) and is otherwise
# informational. "release": nothing experimental runs there, no ad-hoc
# processes, tagged commits only. "dev": any commit, clean or not. Only those
# two are valid; load_env defaults to "dev".
ROLE=release
# --- sizing (docs/design/infra.md section 1) --------------------------------
# CORES must match the width of CPUSET below. If you widen or narrow the
# cpuset, change this too.
CORES=8
MEMORY_MB=8192
# 0, not the design's 2048: a container swap allocation buys nothing on a host
# with no swap of its own — there is no backing store for it. Per-unit
# MemoryMax plus bin/fly-watchdog is then the whole OOM story. Kept as a
# variable rather than deleted because 01-create-ct.sh passes it to
# `pct create --swap`.
SWAP_MB=0
ROOTFS_GB=24
STATE_MP_GB=16 # mp0, SSD pool, /srv/fly/state
MEDIA_MP_GB=600 # mp1, bulk array, /srv/fly/media (ZFS quota)
# --- GPU (docs/design/gpu.md) -----------------------------------------------
# GPU=1 switches on the whole GPU path: 01-create-ct.sh converges the NVIDIA
# passthrough block and the cpuset into the container's conf, 02-base.sh
# installs the matching userspace driver in the container, flycast encodes with
# h264_nvenc, and verify.sh runs its GPU section. GPU=0 makes every one of
# those steps a clean no-op.
GPU=0
# nvenc | x264. Read by bin/flycast-launch out of /etc/fly/fly.env. nvenc falls
# back to x264 automatically and once, if the session cannot be opened (the
# card is shared with another guest — gpu.md section 5), and exports
# fly_encoder_backend so the degradation is visible. Keep it consistent with
# GPU above: a container with no passthrough has no NVENC session to open.
FLY_ENCODER=x264
# NVENC preset, p1 (fastest) .. p7 (best). p4 is the starting point; gpu.md
# section 4 says try p6/p7 once NVENC is in play. Unused while FLY_ENCODER=x264.
FLY_NVENC_PRESET=p4
# x264 preset, the FLY_ENCODER=x264 counterpart, written into /etc/fly/fly.env
# as FLY_X264_PRESET and read by bin/flycast-launch's encoder_x264(), which
# defaults to veryfast when unset. A faster preset is the cheapest way to hand
# CPU back to the sim when the box cannot hold real time; it costs quality per
# bit at the same bitrate, and gpu.md section 4's quality rails were shot at
# veryfast, so a faster preset means they no longer describe what goes out.
X264_PRESET=veryfast
# The host's driver version. The in-container userspace must match the host
# kernel module EXACTLY when GPU=1 — see infra/docs/runbook.md "GPU driver
# version lockstep". 02-base.sh installs
# ${HOST_STAGE_DIR:-/root}/NVIDIA-Linux-x86_64-$NVIDIA_VERSION.run and verify.sh
# asserts the container, this value and the host's nvidia-smi all agree.
NVIDIA_VERSION=<driver-version>
# WHOLE physical cores on ONE socket, SMT siblings deliberately excluded.
# 01-create-ct.sh writes this as `lxc.cgroup2.cpuset.cpus` and 05-deploy.sh
# partitions it between the units via lib/common.sh's cpuset_partition. Two
# measured results shape it: the sim must own whole physical cores (a thread
# landing on an SMT sibling costs about a fifth of real time), and the browser
# must not share a core with the encoder (the compositor starves). Read
# `lscpu -e` on your own host to find which cpu numbers are whole cores on one
# socket; CPUMEMS pins the pages to the socket the threads run on.
CPUSET=<whole-cores-on-one-socket>
CPUMEMS=0
# RAYON_NUM_THREADS for flysim, written into /etc/fly/fly.env by 05-deploy.sh.
# More is not better: past the knee, extra threads are the signature of
# memory-bandwidth saturation and real time gets WORSE. With FLY_LIF_CUDA=1 the
# sweep and the propagation leave the CPU entirely and this drops to about 2.
# The measured numbers for a given box are a record, not a constant — they live
# in the operator's infra repo.
RAYON_THREADS=3
# ENCODER_CORES for flycast, written into its AllowedCPUs= drop-in by
# cpuset_partition (default 2 when unset). flycast gets its own group rather
# than sharing the page group — see lib/common.sh's cpuset_partition header.
ENCODER_CORES=2
# --- CUDA LIF backend -------------------------------------------------------
# 1 makes 05-deploy.sh write FLY_LIF_CUDA=1 into /etc/fly/fly.env, which is what
# flysim reads to attach the GPU LIF backend at startup; 0 (the default for any
# env file that does not set it) runs the CPU kernel exactly as before.
#
# Three things have to be true for this to do anything, and all three are
# checked rather than assumed:
#
# 1. the binary was built with the feature — FLY_CARGO_FEATURES=cuda
# infra/build/build-flysim.sh. A binary WITHOUT the feature ignores this
# variable SILENTLY, which is why a deploy record checks flysim's log for
# the "cuda backend attached" line rather than trusting the env file;
# 2. GPU=1 above, so the container has /dev/nvidia* and the userspace driver;
# 3. the card is the one the committed PTX targets — sm_75. Another
# architecture needs cuda/build-ptx.sh re-run with ARCH=.
#
# Bit-exact with the CPU kernel, so it changes neither the kernel version string
# nor the compatibility string, and a checkpoint moves between a CUDA build and
# a CPU build in both directions with no migration.
FLY_LIF_CUDA=0
# Chromium flag profile: `default` (the --disable-gpu 2D-canvas build, paper
# fly), `gpu` (config/chromium-flags.gpu — measured insufficient on its own: on
# Xvfb it gets no GL at all), or `vgl` (config/chromium-flags.vgl plus
# `vglrun -d egl0`, gpu.md section 3 option (d)), which is how the page gets
# hardware WebGL and about a third of a core back.
#
# Leave it unset on a release container: `vgl` requires --disable-gpu-sandbox,
# because VirtualGL's faker cannot open its second X11 connection inside a
# sandboxed GPU process. The page renderer's sandbox is untouched, but the GPU
# process loses its namespace sandbox while holding an ioctl handle on a kernel
# module shared with another guest. Fine on a dev box; a deliberate decision
# for a 24/7 public stream.
CHROMIUM_PROFILE=
# VirtualGL, required by CHROMIUM_PROFILE=vgl (02-base.sh installs it when GPU=1
# and this is set). Debian has no virtualgl package, so this is an upstream .deb
# staged on the host by hand as
# ${HOST_STAGE_DIR:-/root}/virtualgl_${VIRTUALGL_VERSION}_amd64.deb.
# VIRTUALGL_DEB_SHA256 is mandatory and 02-base.sh refuses to install anything
# else. Upstream publishes no checksum file, only a GPG signature inside the
# .deb's own `_gpgorigin` ar member — verify that against the upstream key
# before writing a hash here (the procedure is in the VirtualGL spike record).
VIRTUALGL_VERSION=
VIRTUALGL_DEB_SHA256=
# --- game selection ---------------------------------------------------------
# FLY_GAME on the flysim unit is set from this. pokemon-red | platformer. The
# GAME id folds into the flysim compatibility string, so a checkpoint from one
# game is rejected by a run of the other.
GAME=pokemon-red
# The ROM itself is never in git, never on the host, never on stream, never
# linked. This is only its sha256, which is what flysim checks the file it
# loads against (FLY_ROM/FLY_ROM_SHA256 in /etc/fly/fly.env, written by
# 05-deploy.sh from this value). Stage the ROM in the container as
# /srv/fly/rom/<sha>.gb, 0400 fly:fly.
#
# Leaving this EMPTY is a trap worth knowing about: 05-deploy.sh then writes no
# FLY_ROM at all, flysim falls back to its own default path
# (/srv/fly/rom/pokemon-red.gb, which no script here creates), and systemd walks
# the unit into the start limiter — but only on the first flysim RESTART after a
# deploy regenerates fly.env, which is the worst possible time to find it.
ROM_SHA256=
REWARD_ADAPTER=fly-pokered-rstdp-v1
# Sustained button holds vs menu-driven pacing is a reviewed change, not a
# config tweak (docs/design/infra.md section 6). Keep the name here so the env
# file is the single place that selects it. default | platformer.
DECODER_PRESET=default
# --- twitch -----------------------------------------------------------------
# Empty is valid and means "this container never pushes anywhere outside
# itself" — the shape a throwaway dev box wants.
TWITCH_CHANNEL=<twitch-channel>
# The `pass` entry holding the stream key. Only the NAME is here; the secret
# never is. 06-secrets.sh reads it on the operator box and installs it as a
# systemd credential.
PASS_KEY=twitch/<channel>-key
# --- flybridge (services/bridge/src/config.ts) ------------------------------
# 05-deploy.sh writes these into /etc/fly/fly.env, which flybridge.service reads
# through EnvironmentFile=. None of them is a secret: the Twitch app id/secret
# arrive as the systemd credential twitch-app (06-secrets.sh) and the OAuth
# tokens as /var/lib/flybridge/tokens.json, mode 0600.
#
# The bot and the broadcaster may be the SAME account — supported
# (services/bridge/src/auth.ts builds one auth provider per role precisely so a
# single account holding both roles cannot lose one of its two tokens) — but
# then the fly's own chat replies come from the channel's own name. Give the bot
# its own account and change this one line.
TWITCH_BOT_USER=<twitch-bot-user>
# The only game-specific word in flybridge's config: substituted into every chat
# template (docs/stream-mvp-plan.md, "keep game words out of code"). ASCII on
# purpose, not an accented spelling: the sanitizers on both sides would pass it,
# but the on-screen CHAT panel is set in a pixel face with no guarantee of a
# precomposed accented glyph, and an unrenderable glyph on the broadcast is
# worse than a missing accent. It is NOT the ROM's file name.
GAME_TITLE="Pokemon Red"
# Channel Points and Predictions are Affiliate-gated by Twitch. With
# FEATURE_REDEMPTIONS=1 on a non-Affiliate channel the API answers 403; the
# bridge degrades that one specific 403 to "redemptions disabled" and keeps
# chat, follows and raids running
# (src/redemptions.ts's isChannelPointsAffiliateRefusal), so flipping this to 1
# early is safe — it logs and carries on until the channel qualifies.
FEATURE_REDEMPTIONS=0
# Predictions stay OFF and their scope stays unused: src/predictions.ts is a
# deliberate stub.
FEATURE_PREDICTIONS=0
# The bridge end of the on-screen CHAT panel. FLY_CHAT_ENABLED below is the
# other end (flysim's), and the one to reach for in a hurry — it needs no Twitch
# credentials to take effect.
FEATURE_ONSCREEN_CHAT=1
# Quiet mode: the bridge speaks only when spoken to. No startup or recovery
# notice, no explainer rotation, no follow/raid thanks; commands and the
# on-screen CHAT panel are unaffected.
FEATURE_QUIET=1
# --- EventSub self-healing (services/bridge/src/subscription-health.ts) -----
# A connected websocket is not a working subscription: both EventSub sockets can
# drop, Twitch can refuse the re-created channel.chat.message subscription with
# "number of websocket transports limit exceeded" (429), and the bridge then
# stays `active (running)` with NO chat subscription. It exits 75 when that
# subscription has been unconfirmed for this long, and flybridge.service
# restarts it 15 s later with a fresh transport. 60 s is generous: a healthy
# reconnect confirms in well under a second. Lower it and a slow reconnect
# becomes a needless restart; raise it and the channel stays dead longer.
EVENTSUB_GRACE_MS=60000
# At most one startup-or-recovery notice in chat per this window, counted across
# process restarts, so a bad half hour on Twitch's side cannot turn the
# self-healing restarts into chat spam.
NOTICE_MIN_INTERVAL_MS=600000
# --- stream output ----------------------------------------------------------
# 1920x1080 / 6000 kbps is the current decision (docs/streaming-plan.md section
# 3's dated note). Not yet consumed by infra/units/xvfb.service,
# infra/config/chromium-flags or infra/units/flycast.service, which still
# hardcode these same numbers directly (05-deploy.sh does not template them into
# /etc/fly/fly.env yet) — this is the single documented source of truth for the
# three numbers until that wiring lands.
STREAM_WIDTH=1920
STREAM_HEIGHT=1080
STREAM_KBPS=6000
# --- on-screen chat (docs/control-api.md [chat]) ----------------------------
# The persistent CHAT panel in the right rail. 1 = on, 0 = the kill switch:
# POST /chat answers 403 and the feed header omits `chat`, so the panel blanks
# without touching flybridge or the page.
#
# Chat never reaches the simulation. It is sanitized by flybridge, sanitized
# AGAIN by flysim, deny-list filtered and rate-limited there, and carried in a
# 12-line ring in the feed header.
FLY_CHAT_ENABLED=1
# Operator-maintained deny list, one pattern per line. 05-deploy.sh installs
# infra/config/chat-deny.txt here ONCE and never overwrites it; edit it in the
# container and run `systemctl kill -s HUP flysim` to re-read it (flysim also
# re-reads it once a minute by itself).
CHAT_DENY_LIST=/srv/fly/chat-deny.txt
# --- macro buttons ----------------------------------------------------------
# What the fly's channels mean (docs/design/macros.md section 12,
# docs/control-api.md).
# raw: the population decoder's button mask goes to the emulator.
# macros: the scene's macros are buttons of their own. Every macro type has its
# own neuron population (macro_<type>, mushroom body output neurons plus
# brain motor neurons) and its own channel in the readout's second
# exclusive group; the scene decides which of those channels exist and
# the decoder picks among exactly those, on the direction group's own
# hold, hysteresis and fatigue. The eight real buttons are decoded as
# ever, and while a macro runs it owns the pad.
# "palette" and "plan" are the two modes section 12 replaced; flysim still reads
# either as "macros", with a warning, for one release.
FLY_MACRO_MODE=raw
# How long a macro leaves a target alone after a walk to it aborted "blocked" or
# "timeout" (macros.md section 12.1). Session state, so a restart offers every
# target once more. Unset means the default, 10.
# FLY_MACRO_BLOCKED_MINUTES=10
# --- push mode --------------------------------------------------------------
# local: flypush.service stays disabled, everything else identical to prod.
# twitch: flypush.service is enabled by 07-enable.sh.
#
# Never go live without the operator's explicit approval for that run
# (CLAUDE.md). `local` is the safe default and local MediaMTX demos are fine.
PUSH_TARGET=local