add AGENTS.md
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>
This commit is contained in:
1 parent
fc2c08d47b
commit
3a3b4b8eb9
1 file changed
+246
@@ -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@<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.
|
||||
Reference in new issue
Block a user