# 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 # # 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 (``, ``, …) 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= # 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_ROOT= FLY_BACKUP_DIR= # --- container identity ----------------------------------------------------- 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= 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= # 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= 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/.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= # 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/-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= # 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_, 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 # --- feed path ---------------------------------------------------------------- # Who serves ws://127.0.0.1:7400/feed (docs/design/flybus.md, "Feed over the # bus"). direct: flysim binds it, as always. bus: flysim publishes every # snapshot on its embedded feed bus (/run/fly/bus) and flyedge.service serves # the same bytes; enable that unit by hand (its header has the steps). # Watchdog check 2 follows this setting to the edge's counters by itself. FLY_FEED_VIA=direct # 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 # --- restoring across an adapter version ------------------------------------ # Adapter version strings whose checkpoints this build may migrate, comma- or # space-separated (docs/design/flysim.md, "Restoring across an adapter # version"). Unset -- the default, and what every deploy before 2026-09-22 did # -- migrates nothing: a build whose compatibility string differs from the live # state's is refused by 05-deploy's gate and by flysim at restore. # # It applies only when the ADAPTER segment is the only difference between the # two strings AND the new build's adapter declares a migration from that one. A # dataset, kernel, plasticity, emulator or state-format difference is still a # refusal. Set it for the one deploy that needs it and leave it out afterwards; # 05-deploy writes it into /etc/fly/fly.env only while it is set. # FLY_ACCEPT_ADAPTERS=pokered-unique8-v5 # --- 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