# AGENTS.md Guidance for AI agents (and people) working on this repository. ## What this is `terraria-server-manager` runs any number of vanilla Terraria and tModLoader dedicated servers on one Linux host, each as its own systemd service (`terraria@.service`), managed with the `terractl` command. It ships as a Debian package. Server software is never bundled: official zips go in `/var/lib/terraria/dist` and are unpacked into each instance on demand. The user-facing documentation is `docs/guide.md` (installed as the package's README). Read the sections relevant to a change before making it; the guide is the specification of how things behave. ## Layout | Path | What it is | |---|---| | `bin/terractl` | The main tool, in bash. Everything on the local host. | | `lib/terractl-remote` | Python (standard library only, 3.9+): `--host`, `remote`, `daemon`, `copy`, `move`. Its daemon does everything by running `terractl` on its own host. | | `completions/bash/terractl` | Bash completion. | | `systemd/` | `terraria@.service`/`.socket` (one per instance), `terractld.service` (daemon for other hosts), `terraria-backup@.service`/`.timer` (scheduled backups). | | `config/` | `default/terractl` (user overrides, a conffile), `tmpfiles.d`, `sysusers.d`, `polkit` rule, `journald@terraria.conf` (a conffile). | | `packaging/deb/` | `control`, `conffiles`, maintainer scripts (`postinst`, `prerm`, `postrm`). | | `packaging/gitea-release.py` | Publishes a release with that version's CHANGELOG section as notes. | | `.gitea/workflows/release.yaml` | On a `v*` tag: `make deb`, publish the release and the apt repository. | | `tools/windows/export-tmod-mods.ps1` | Helper for players to export their enabled tModLoader mods. | | `docs/guide.md`, `README.md`, `CHANGELOG.md`, `VERSION` | Docs and release metadata. | Placeholders `@VERSION@`, `@BINDIR@`, `@DOCDIR@` and `@PKGLIBDIR@` in `bin/terractl` and the unit files are filled in by `make install`/`make deb`. A copy run straight from the repo is a "development copy": it detects the unfilled placeholders and uses `lib/terractl-remote` from the source tree. ## Build and check ```bash make check # bash -n, sh -n on maintainer scripts, Python syntax, shellcheck, pyflakes make deb # runs check, builds build/terraria-server-manager__all.deb ``` **`make check` must pass with shellcheck installed** (`apt install shellcheck python3-pyflakes`). The release workflow runs it, so a shellcheck warning blocks a release. Fix warnings rather than disabling them; a `# shellcheck disable=` needs a comment saying why (see the existing ones). There is no automated test suite. See [Testing](#testing). ## How the system fits together These invariants run through the whole code base. Keep them. - **Servers run as the unprivileged `terraria` user**, sandboxed by `terraria@.service` (`ProtectSystem=strict`, `ProtectHome=yes`, writable only `ReadWritePaths=/var/lib/terraria/instances`, `UMask=0027`). - **Nothing in an instance may end up owned by root.** Commands run as root `chown` what they create (`fix_owner`, `chown -R` after `new`/`import`). `prepare` and `run` started as root switch to `terraria` with `runuser` (re-running `$SELF`, which is resolved before the script's `cd /`). - **Permission model:** root can do everything; members of the `terraria` group can read instances, start/stop/restart servers (polkit rule), use the console (`attach`, `cmd`, `watch`), read the servers' own journal (log namespace `terraria`), add zips to `dist/` and read backups. Group members can read but never change instance files. Every new command must fit this model and be added to the permissions table in the guide. - **`instance.conf` is sourced by bash.** Anything written into it (e.g. `ZIP=`) must be validated first (`safe_zip_name`). Instance names are validated by `valid_name` and can't start with `.`. - **One server per instance at a time:** the lock on `/.lock` (fd 9) is held for the server's lifetime. Commands that replace `server/` or delete things under a running server must check `instance_running`. - **`server/` is replaceable, `data/` is precious.** Unpacking happens in a temp folder and only replaces `server/` once complete; `server/.installed` records ` `. Nothing may delete or rewrite worlds without a backup. - **Changes that can lose data back up first** and leave things as they were on failure (see `cmd_upgrade`: validate everything, stop, back up, change, roll back the pin and restart on failure). - **Files outside `/var/lib/terraria`:** only the documented drop-ins in `/etc/systemd/system` (`terraria-backup@.{timer,service}.d/terractl.conf` from `backup enable`, `.d/terractl-locations.conf` from `doctor --fix`). `postrm` purge removes them. Never overwrite admin files or unit files. - **The daemon (`terractld`) runs `terractl` as root** with `TERRACTL_VIA` set. Only commands listed in `EXEC_COMMANDS` in `lib/terractl-remote` can be run remotely. It runs with `ProtectSystem=full` and `ProtectHome=true`, plus write access to `/etc/systemd/system` only. - **Locations are configurable** (`TERRARIA_BASE`, `TERRARIA_DIST`, `TERRARIA_BACKUPS` in `/etc/default/terractl`), but unit files can't follow them; `location_warnings` and `doctor` detect mismatches. Never hard-code `/var/lib/terraria/...` in `bin/terractl`: use `$BASE_DIR`, `$DIST_DIR`, `$BACKUP_DIR`. ## Code conventions ### bash (`bin/terractl`) - `set -euo pipefail`. Be careful with pipelines that can legitimately fail (`grep` with no match, `grep -q` closing a pipe early): capture to a variable first, or add `|| true`. - Errors: `die "message"` (exits 1); notes: `log "message"` (stderr). Both prefix `terractl:`. Normal output goes to stdout. - **Messages say what to do next.** When suggesting a command, use `hint root|group [args]`, which adds `sudo` only when needed and shows `terractl --host ...` when run through the daemon. - Commands are `cmd_` functions; sub-actions are `_` (e.g. `backup_enable`). Helpers are lower-case with underscores. Sections are separated by `# ---- name ----` banner comments. - Every non-obvious function gets a short comment above it saying what it does and why; match the existing density (comments explain intent and edge cases, not syntax). - Option parsing: a `while`/`case` loop, `die "unknown option '$1'"` for anything unexpected, and a `usage: terractl ...` message for wrong arguments. - Root checks: `[[ $EUID -eq 0 ]] || die "'' needs root: sudo terractl ..."`. - `local` for all function variables. Globals are upper-case. - `EXTRACT_TMP` is the single temp-folder slot that `cleanup` (the EXIT trap) removes. Code that must survive a `die` in a helper runs it in a subshell with its own `trap cleanup EXIT` (see `make_backup`). ### Python (`lib/terractl-remote`) - Standard library only, Python 3.9+. `make check` runs pyflakes on it. - It never touches instances directly: it runs `terractl` (locally, or via the daemon on the other host) so checks and ownership stay in one place. ### Shell in packaging - Maintainer scripts are POSIX `sh`, idempotent, and must never fail an install or removal because systemd isn't running (`[ -d /run/systemd/system ]` guards, `|| true` on best-effort steps). ## Adding or changing a command: checklist 1. `bin/terractl`: the `cmd_*` function, the `usage` text, and the dispatch at the bottom. Commands that don't take an instance name first (like `list`, `doctor`, `backup`, `fetch`) are dispatched in the first `case`, before `valid_name "$1"`. 2. `lib/terractl-remote`: add it to `EXEC_COMMANDS` if it should work with `--host` (and a handler in `run_on_remote` if it moves files to or from the caller's computer, like `export`, `import` and `mods add`). 3. `completions/bash/terractl`: the command list (`commands=`), and its arguments/options. Local-only commands go in the `else` branch's list. Instance names on a remote come from `_terractl_remote_instances`. 4. `docs/guide.md`: - the section describing it (with examples using the `calamity` instance); - the **Permissions** table; - the **Tab completion** list, if completion changed; - the **Command reference**; - **Troubleshooting** and the `doctor` description, if relevant; - **Components** and **Directory layout** for new files or folders. 5. `README.md` features list, for user-visible features. 6. `CHANGELOG.md`: an entry in the newest section (see [Changelog](#changelog)). 7. New installed files: `Makefile` `install` **and** `uninstall`, unit files through `$(SUBST)` if they contain placeholders, `packaging/deb/postrm` (purge) for anything created at runtime, and `conffiles` for files in `/etc` that users edit. 8. If it affects what can go wrong on a host, consider a check in `doctor` (`doc_problem warn|FAIL "" "" []`; only pass a fix function for fixes that are safe to apply unattended). ## Testing Agents usually can't run as root, use systemd, or install the package. The pattern used so far: - Make a scratch folder (outside the repo) with `inst/`, `dist/`, `backups/` and point `TERRARIA_BASE`, `TERRARIA_DIST`, `TERRARIA_BACKUPS` at them, and `TERRARIA_USER` at your own user. - Fake root in a **copy** of the script: `sed -e 's/\$EUID/${FAKE_EUID:-$EUID}/g' bin/terractl > scratch/terractl` with `FAKE_EUID=0` exported, and a `runuser` stub on `PATH` that drops to the "service user" with `exec env -u FAKE_EUID "$@"` after the `--`. (Replacing `$EUID` with a literal `0` makes `prepare` re-run itself forever.) - Stub `systemctl` on `PATH` that keeps unit state in files (`is-active`, `is-enabled`, `is-failed`, `start`/`stop`/`enable`/`disable`, and `show -p` for the properties used). Point `SYSTEMD_ETC` at a scratch folder in the copy. - Build test zips with Python's `zipfile` (`zip` may not be installed): vanilla needs `/Linux/TerrariaServer.bin.x86_64`, tModLoader needs `start-tModLoaderServer.sh`. - `fetch` can be tested against the real sites; downloads are 40-60 MB. - Don't `pkill -f` a pattern that appears in your own command line. - Always run `make check`, and `make deb` when packaging changed. For unit files, run `systemd-analyze verify` on the **built** copies under `build/deb-root` (the sources contain placeholders). Say clearly what was tested with stubs and what wasn't (real root, real systemd, real servers); the maintainer tests on a real host before releasing. ## Documentation style - Plain, direct English; short sentences; British spelling where it differs. - Explain what happens and why, then show the command. Examples use the instance `calamity` and the remote `nas`. - Bold lead-in phrases for key points (`**Downgrades need --downgrade.**`). - Command reference and usage text are aligned two-column blocks; keep the columns aligned when adding lines. - Documented behaviour must match the code; update both in the same change. ## Changelog `CHANGELOG.md` is for users, and each version's section is published as-is as its release notes on the Gitea release page (`gitea-release.py`). Write it so it reads well there. - **Headings:** `## - unreleased` while in progress, `## - ` once released. `gitea-release.py` takes the first word after `## ` as the version, so keep exactly that shape. - **Where entries go:** in the newest section if it's unreleased. If the newest section is already released (dated), start `## - unreleased` at the top **and** bump `VERSION` to match, in the same commit. - **Released sections are history:** don't edit them, except to fix a typo. - **Unreleased changes are edited, not appended to.** If something in the open section changes before release (renamed, moved, fixed), update its existing entry so the section describes the final state, rather than adding a "fixed X" line about something users never had. - **What an entry says:** the command and options as the user types them (`` `sudo terractl backup enable [--every ]` ``), what it does and what they'll see, defaults, and anything they must do themselves (restart servers, run a command after upgrading, a new dependency). Full sentences; say "now" for changed behaviour and "before, ..." when the old behaviour could have caught people out. - **Order:** new features first, then changed behaviour, then fixes; the most important first within each. A large feature gets one bullet with sub-bullets for its parts (see `upgrade` and `fetch` in 0.0.12). - **Leave out** refactors, test-only changes, code style and doc rewording, unless the docs gained something users should know about (e.g. a new guide section on a setup that didn't work before). - Every user-visible change gets an entry in the same commit as the change. ## Releases - `VERSION` holds the version (`0.0.N`) and must match the newest changelog section (see [Changelog](#changelog)). - To release: set the date, commit, then tag **`v`** (annotated, message `terraria-server-manager `). The workflow refuses a tag that doesn't match `VERSION`. Pushing the tag builds and publishes the release and the apt repository, so only push when asked. ## Git - Commit subjects are short, lower-case and imperative-ish (`add upgrade command; reinstall keeps the same version`), with a body listing the changes for anything non-trivial. - Everything is LF (`.gitattributes`); `build/`, `*.deb` and `__pycache__/` are ignored. - Commit, tag or push only when asked.