diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0e1e52e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,246 @@ +# 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.