Guidance for agents working on the repo: layout, build and checks, the invariants (ownership, permissions, sandbox, data safety), code conventions, a checklist for adding commands, testing without root, docs style, the changelog, releases and git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
13 KiB
Markdown
247 lines
13 KiB
Markdown
# 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@<name>.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_<version>_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 `<instance>/.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
|
|
`<type> <zip>`. 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@<name>.{timer,service}.d/terractl.conf`
|
|
from `backup enable`, `<unit>.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 <command> [args]`, which adds `sudo` only when needed and
|
|
shows `terractl --host <remote> ...` when run through the daemon.
|
|
- Commands are `cmd_<name>` functions; sub-actions are `<command>_<action>`
|
|
(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 "'<command>' 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 "<what>" "<fix command>" [<function that fixes it>]`;
|
|
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 `<ver>/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:** `## <version> - unreleased` while in progress,
|
|
`## <version> - <YYYY-MM-DD>` 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 `## <next version> - 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 <name> [--every <when>]` ``), 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<version>`** (annotated,
|
|
message `terraria-server-manager <version>`). 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.
|