Repository navigation
Developers
This document is the technical reference for contributors, maintainers, and automation working inside the repository.
Release-facing changes should also update changelog.txt.
The skill catalogue covers operations and repository development.
Canonical manifests stay in skills/<name>/SKILL.md; relative per-directory
links in .agents/skills/ support native discovery without copied instructions.
Each manifest has YAML name/description, a focused trigger, required inputs,
real command/source surfaces, bounded failure handling and completion evidence.
Keep descriptions concise and load references only when relevant.
The server-requirements skill discovers prerequisites for the selected canonical module/version across manager, installation, runtime, provider/assets, settings and host resources. Use the capability inventory as a routing index, then inspect actual hooks and transitive installer/build paths; undeclared or skipped checks remain unknown. Declare real host dependencies and phase-specific provider requirements when maintaining a module, and keep its guide aligned.
The installation and request policy limits hard approval gates to new software installations. AlphaGSM code, repository helpers/images and their builds, project-local pinned requirements, and supported managed components proceed within the task without extra approval. Ask before genuinely new external host software such as Docker or a compiler. For vague game requests, ask to install if absent or clarify the action/target if already installed. Explicit existing-server actions proceed. These are agent instructions; they do not add an interactive consent mechanism to the CLI.
After changes, run make skills-check and the validator unit tests:
PYTHONPATH=.:src python3 -m pytest tests/unit_tests/test_agent_skills_validation.pyThe read-only validator checks YAML, unique names, catalogue membership, inline local Markdown links and discovery targets. It uses the development-only PyYAML dependency from requirements.txt. It performs no network/server actions. Valid Git symlink stubs are accepted with an explicit-loading notice for platforms without working symlinks; this does not establish native discovery there.
Structural checks do not evaluate agent decisions. Use skill-maintenance and its scenario reference to test realistic requests with neutral inputs and disposable data. Record baseline gaps, actual outcomes, corrections and execution mode. Keep walkthrough evidence separate from real smoke/integration/backend lifecycle results. Every new skill needs a task-specific evaluation; risky restore/reset, port-ownership and provider paths require concrete scope and failure evidence.
See the operator guide for user-facing setup. Skills operate within the user's task; they do not create implicit monitors or authorize world deletion, releases or unrelated server changes.
AlphaGSM is a Python CLI that normalises game-server lifecycle management across multiple backends. The core design is:
- one shared command surface
- one persistent per-server datastore
- per-game module implementations for lifecycle details
- a runtime layer that can launch either local screen-backed processes or Docker containers
- dedicated test layers for unit, integration, and streamed smoke coverage
core.main routes only bare invocation, --welcome and --skip-welcome to
core.welcome. Automatic prompting requires both stdin and stdout to be TTYs
and no acknowledgement marker. All named server/agent commands, internal workers,
help and version follow their usual paths. Keep this boundary so automation
never blocks on first-run questions.
The tour passes argv arrays back through ordinary dispatch after previews and
explicit approvals. It resolves game aliases through MODULE_CATALOG, restricts
local names, and rejects core.server_names.RESERVED_SERVER_NAMES, numeric selectors
and all before dispatch. Never let wildcard/count interpretation bypass the
existing-record check. Creation checks lexists before the preview and again
under the ordinary per-record state lock, refusing existing files or links.
Setup has separate approval; games never start from the tour. Optional agent
setup has a separate network confirmation and keeps its own enrollment/binding
permissions; the tour does not call capability-enable commands.
core.welcome_state owns the non-secret, versioned welcome-v1.done marker under
configured core.alphagsm_path. Creation is exclusive, mode 0600, with no-follow
where available, ownership checks and no overwrite of existing markers. It
preserves existing directory permissions and keeps acknowledgement separate from
server/agent configuration and all consent. EOF/Ctrl+C stops further commands;
completed approved actions are not rolled back.
Run the isolated first-run smoke checks with:
PYTHON_BIN=python3 bash tests/smoke_tests/run_first_run_tour.shThis covers unit boundaries plus actual CLI subprocesses and POSIX PTYs against
disposable config/home paths, including the all overwrite regression. PTY cases
require POSIX and skip on platforms without PTYs. It neither downloads game
assets nor starts servers, and does not establish game lifecycle support.
Keep repository docs split by audience and preferred operator path:
- README.md should stay simple and lead users toward the Docker manager quick-start first, then the host Docker-runtime path, with direct host-process usage documented as the fallback.
- docs/ should hold the primary user-facing workflows and server guides. After the Docker-first migration, docs/docker-manager.md and docs/docker-runtime-host.md are the main runtime-entry docs.
- DEVELOPERS.md is for implementation details, contracts, architecture, and contributor-facing guidance.
The operator-facing walkthrough lives in docs/adding-a-game-server.md. Use that page as the contribution entry point. This section is the technical checklist.
A new top-level game is a package under src/gamemodules/<module_id>/ with
__init__.py as the public import surface and main.py as the implementation.
Users then run the same commands they already know: create, setup, start,
status, query, info, stop.
Copy the closest working module:
- Palworld — custom SteamCMD download plus game-specific settings layout
- HL2DM —
define_valve_server_module()plus shared runtime port constants - Minecraft Vanilla — Java runtime family
Set module_contract_version = 1 on the public surface so AlphaGSM can reject
malformed hook exports before runtime inference. Version 1 does not prove that
the dedicated server starts.
A module is incomplete until it lands with:
- required lifecycle and Docker hooks on the public import surface
- unit tests under
tests/unit_tests/ - an integration test under
tests/integration_tests/test_<module>.pythat drives AlphaGSM commands through create, setup, start, readiness, status, query, info, info --json, and stop - a smoke runner under
tests/smoke_tests/run_<module>.sh -
docs/servers/<module_id>.md, a changelog entry, and a tracker update once the lifecycle is actually proven
Do not treat hibernation, TCP fallback, or a skipped timeout as a pass.
At runtime, the user-facing call path is:
./alphagsm ...- src/core/main.py
- server-name parsing, wildcard expansion, and command parsing
-
src/server/server.py
Server(...) - merge of default commands with module-defined commands
- dispatch into default server behaviour or module-specific functions
- runtime resolution through src/server/runtime.py
- process startup through src/screen or container startup through Docker
- alphagsm User-facing CLI entry point.
- alphagsm-internal Internal helper used for multi-server and delegated execution paths.
- alphagsm-downloads Download-user helper for shared artifact retrieval.
- core CLI dispatch, command routing, subprocess orchestration, multiplexer logic.
-
server
Serverabstraction, datastore integration, default command set, runtime selection, module loading. - gamemodules Game-specific implementations.
- downloader Shared artifact cache and download ownership flow.
- downloadermodules Backend-specific download implementations.
- screen GNU screen orchestration and log helpers.
- utils Settings, backup scheduling, SteamCMD helpers, command parsing, filesystem update helpers.
- tests/unit_tests Unit tests.
- tests/integration_tests Pytest-driven end-to-end tests.
- tests/smoke_tests Shell-driven streamed lifecycle runners used by CI and documentation.
- docs User-facing documentation.
Store per-server config examples under docs/server-templates/<module_name>/.
- Make the template mimic the real game-owned config file as closely as possible: filename, relative path, key spelling, syntax, comments, and section layout.
- Use the real runtime filename when the module or docs identify one stable on-disk config path, such as
server.cfg,server.properties,mumble-server.ini, ordedicated_cfg.txt. - Keep
alphagsm-example.cfgonly when the module exposes AlphaGSM-managed values but does not manage one stable game-owned config filename. - Match the real relative path when it is part of the contract, for example
ROGame/Config/PCServer-ROGame.iniorSystem/UT2004.ini. - Include concrete default values from
configure(...),sync_server_config(...), smoke tests, or the matchingdocs/servers/<module>.mdguide. - Do not include AlphaGSM-only datastore keys such as
download_name,dir,backup,image, or other manager metadata unless the game itself reads them. - Start from
docs/server-templates/_template/when adding new template directories. - Run
python scripts/audit_server_templates.pyafter template changes to catch filename mismatches against module- and guide-backed config paths.
The shared command contract is defined in src/server/server.py.
Default commands (see docs/commands.md for the operator wording):
-
setup,start,stop,restart,kill -
activate,deactivate -
status,query,info,doctor -
send,message,connect,logs -
set,dump -
backup,restore -
wipe,reset-world
Game modules extend this model by exporting module-level data:
commandscommand_argscommand_descriptionscommand_functions
The server object merges these module-level definitions with the defaults at runtime.
A canonical top-level game module is a package directory under
src/gamemodules/<name>/ with an __init__.py import surface. The Server
class dispatches user commands by calling named attributes on the canonical
module import surface. The full specification lives in
src/server/gamemodules.py. The skill-level
checklist lives in skills/server-lifecycle/SKILL.md.
Modules may opt into structural validation by setting
module_contract_version = 1 on the public import surface. Unversioned modules
keep the existing loading path. Version 1 is checked before legacy runtime-hook
inference and proves that required hooks are directly exported callables and
that declared config-sync keys are well formed. It does not prove native config
completeness, provider credentials, signatures, or that the server can start.
Ownership of game-module behaviour stays split:
| Owner | Responsibility |
|---|---|
| Manager | Command sequencing, persistence, port ownership, provider/platform enforcement, runtime selection, and lifecycle error handling. |
| Shared operation | A bounded action such as downloading files, rewriting a particular config format, or constructing a runtime spec. |
| Family builder | Defaults and behavior proven equivalent across an engine family. |
| Game module | Select operations, order them, validate game-specific values, and implement engine/install/runtime exceptions. |
Extract a shared helper only when both the successful semantics and the error behaviour are shared. Keep small duplication when stop/restart sequencing, config rewrites, or game-specific layout work differ. An exception can replace a whole hook or call shared operations in a custom order; it must not require a new global flag, and shared helpers must not branch on canonical game IDs.
Palworld is the custom-install example: it calls the reusable SteamCMD download
operation, then prepares PalWorldSettings.ini itself. It does not wrap the
generic install factory, because that factory also owns optional config sync
and post-download callbacks that Palworld does not use.
def install(server):
os.makedirs(server.data["dir"], exist_ok=True)
installers.download_steamcmd(
server,
steamcmd_module=steamcmd,
steam_app_id=steam_app_id,
steam_anonymous_login_possible=steam_anonymous_login_possible,
)
_finalize_install_layout(server)Half-Life 2: Deathmatch is the family-composition example: it keeps
define_valve_server_module(), combines addon commands, and declares shared
runtime constants once for both Docker wrappers.
module_contract_version = 1
RUNTIME_FAMILY = "steamcmd-linux"
PORT_DEFINITIONS = (
{"key": "port", "protocol": "udp"},
{"key": "port", "protocol": "tcp"},
{"key": "clientport", "protocol": "udp"},
{"key": "sourcetvport", "protocol": "udp"},
)Keep define_valve_server_module() for Valve-family defaults. Do not replace it
with a universal builder, base-class hierarchy, mixin system, or hook event bus.
For top-level game modules, keep the implementation in main.py and reserve
__init__.py for the canonical re-export surface.
Canonical module ids are real top-level package names under src/gamemodules/.
- Define user-facing aliases and namespace defaults in src/server/module_aliases.json.
- Resolve module names through src/server/module_catalog.py before importing a game module.
- Persist the canonical module id in the server datastore even when the user created the server through an alias such as
tf2orcs2server. - Do not add wrapper alias files such as
tf2.pyor namespaceDEFAULT.pyshims undersrc/gamemodules/; that routing now lives in the shared catalog. - Alias keys share the same namespace as canonical ids, and alias values must point directly at a real canonical module id.
- For package-backed canonical modules, the directory name is the canonical id and
__init__.pyis the only public import surface thatServer(...), parity tooling, and static contract tests should treat as canonical. - Keep package internals such as
main.py,mods.py,layout.py, orworkshop.pyas private implementation files. They do not get separate alias entries, parity rows, or standalone module identities. - When converting a file-backed module into a package-backed canonical module, preserve the exported lifecycle hooks from
__init__.pysoimport gamemodules.<module_id>keeps working for existing callers and tests.
Generated parity reporting also works at the canonical-module level:
- scripts/generate_module_parity_report.py
- docs/module_parity_report.md
- docs/module_parity_report.json
- src/server/module_capabilities.json
After changing module capabilities such as config_sync_keys, run
python3 scripts/generate_module_parity_report.py and commit all generated
changes. Verify with python3 scripts/generate_module_parity_report.py --check;
CI runs this separately before the unit suite.
Generated server-support tracking is derived from the checked-in integration status report:
| Attribute | Notes |
|---|---|
commands |
Tuple of extra command names beyond the default set (e.g. ("update", "restart")). Use () if none. |
command_args |
Dict mapping command name → CmdSpec for all extra commands and any built-in commands whose arg spec the module extends. |
command_descriptions |
Dict mapping command name → description string for all extra commands. |
command_functions |
Dict mapping command name → callable for all extra commands. |
| Function | Signature | Backs user command |
|---|---|---|
configure |
(server, ask, *args, **kwargs) |
setup |
install |
(server, *args, **kwargs) |
setup |
get_start_command |
(server, *args, **kwargs) |
start |
do_stop |
(server, time, *args, **kwargs) |
stop |
status |
(server, verbose, *args, **kwargs) |
status |
message |
(server, message, *args, **kwargs) |
message |
backup |
(server, *args, **kwargs) |
backup |
checkvalue |
(server, key, *values, **kwargs) |
set |
get_runtime_requirements |
(server) |
runtime metadata for Docker-capable modules |
get_container_spec |
(server) |
Docker launch spec |
| Function | Signature | Purpose |
|---|---|---|
prestart |
(server, *args, **kwargs) |
Run before the screen session starts (e.g. symlink Steam libraries) |
poststart |
(server, *args, **kwargs) |
Run after the screen session starts (e.g. send init commands) |
postset |
(server, key, **kwargs) |
React after a set data-store change |
setting_schema |
module-level dict[str, SettingSpec]
|
Declare schema-backed set keys, aliases, examples, and secrecy for discovery; entries with secret=True are automatically routed to the mode-0o600 secrets file (see Secrets File) |
config_sync_keys |
module-level tuple[str, ...]
|
List datastore keys that should auto-sync into native game config files |
sync_server_config |
(server) |
Rewrite the native config file from datastore-backed values |
list_setting_values |
(server, canonical_key) |
Return enumerable values for schema-backed keys such as maps |
get_query_address |
(server) |
Return (host, port, protocol) for query; protocols include "a2s", "quake", "source_rcon", "ts3", and "tcp"
|
get_info_address |
(server) |
Return (host, port, protocol) for info; protocols include "a2s", "slp", "source_rcon", and "tcp". Always add this unless the module uses define_valve_server_module(). |
update |
(server, validate=False, restart=False, ...) |
In-place server update; wire into commands / command_functions
|
restart |
(server, ...) |
Custom restart logic if the default stop+start is insufficient |
get_wipe_paths |
(server) |
Return explicit relative world-data paths for the shared preview/confirmation/deletion flow; fixed wipe_paths lists also work |
get_wipe_root |
(server) |
Optional explicit world-data root (default: install directory), e.g. Necesse's configured datadir
|
max_stop_wait |
attribute int
|
Max minutes to wait for graceful stop (default 5, capped at 5) |
Keep native interactive world selection available. Terraria/TShock and Necesse
expose a one-launch start --autocreate option for fresh unattended fixtures;
tests must opt in explicitly. If the configured world path already exists,
including an unreadable file or symlink, the option produces exactly the plain
start command. Existence checks use host paths even when building Docker specs.
Necesse explicitly mounts its data directory and passes the container path to Java.
Minecraft and Rust already generate missing worlds in their normal launch flow.
reset-world and wipe share server.worlds.wipe_worlds. Module hooks only
resolve targets; core code validates relative paths, rejects symlinked targets
and parents, prints the plan, confirms by default (-Y / --yes bypass),
rechecks the stopped state and plan, then deletes those targets. Directory
entries mean recursive removal of their contents. Unpreviewable module wipe
callbacks are no longer executed. Do not return an entire install/config/plugin
root or guess a native save location. See world management.
Curated server-side mod support now has a shared core under src/server/modsupport.
-
registry.pyresolves module-local curated registries from family plus optional channel/version into a concrete release id, URL, allowed hosts, archive type, and approved destination roots. -
downloads.pyowns trusted-host validation, optional checksum enforcement, safe archive extraction, and destination allowlisting so AlphaGSM only installs files into explicitly approved server paths. -
ownership.pyrecords the AlphaGSM-owned relative file manifest for each installed curated entry. -
providers.pyowns shared external-provider helpers such as GameBanana item resolution, Mod DB page resolution, Workshop id validation, and direct URL normalization for plugin-style modules. These providers are not TF2-specific; individual game modules decide whether and how to install their payloads safely. -
workshop.pyresolves positive uint64 IDs through Valve's public metadata APIs, normalizes numeric IDs and official Steam Community filedetails URLs at input boundaries while keeping returned API IDs strictly numeric, validatesconsumer_app_idagainst the module's game Workshop AppID, and expands collections iteratively with order preservation, deduplication, cycle detection, response-size limits, node/request limits, and bounded transport retries. A collection must validate completely before intake saves any additions. Collection provenance is stored on ordinary desired Workshop entries; imports are snapshots and do not remove previously requested items. Legacy metadata withoutfile_typeuses content fields to identify items and verified collection details to identify collections. Opaque zero-size entries with a content handle probe collection identity: only a validated result 9 permits item fallback; transport failures remain errors. Collection branding image URLs are not downloadable item payloads. -
source_addons.pyrevalidates desired Workshop metadata on apply, including legacy/manual entries, in batches under one resolver's request budget. Each completed installation persists ownership before proceeding so a later download failure leaves earlier files tracked for cleanup. Staging rejects destination symlinks and paths outside the server root before copying. Modules must declareworkshop_app_idindependently of their dedicated-server Steam app ID. L4D2 uses550for Workshop and222860for server installation, and opts into a single bare.vpkpayload installed asaddons/workshop/<item-id>.vpkthrough owned-file staging. -
workshop_downloads.pysupplies the sharedworkshop download item|collection ID_OR_URLandworkshop listcommand on every anonymous SteamCMD module. Command availability reads module-owned anonymous metadata, including the Valve factory namespace; editable datastore auth flags cannot enable it. The game identity comes from moduleworkshop_app_id, the configured Source game'sgameinfo.txtSteamAppId, or bounded discovery of upstreamsteam_appid.txtfiles, excluding mod/Workshop caches. Missing, invalid, or conflicting identity fails closed. Verified downloads are copied into.alphagsm/workshop/content/<app>/<item>with per-item completed-state persistence. Fresh staged payloads replace previous downloads with rollback on publication failure; obsolete upstream files do not accumulate. Cache symlinks are checked before SteamCMD runs. This shared path stages content; module adapters own activation. -
native_workshop.pyshares game-validated item/collection intake and native configuration state across game-specific adapters. Adapters receive a completely validated ordered ID list;native_workshop.status=configureddescribes declarations, not engine load evidence. Contained nonsymlink paths and atomic text publication protect native configs. Cleanup calls the adapter with an empty list before clearing desired entries. Module callbacks own preservation/removal of operator config and multi-file rollback. -
owned_payloads.pypublishes contained native files as a transaction with SHA-256 ownership records. It rejects operator files, edited owned files, symlinks and overlapping paths before publication. Failed publication restores previous assets and ownership; failed rollback retains original backups and a recovery manifest. Adapters also restore their native config and metadata if publication fails. Cleanup preserves unowned files and download caches. - Each native loader's live fixture and process/Docker status is recorded in the Workshop support matrix. Require fresh engine mount, map, registry or mod execution evidence in addition to query/info and shutdown; configured desired state is not engine load proof.
- Wreckfest's Linux baseline uses query-port UDP reachability
plus a fresh native
Server connected.log and running runtime; it provides no map or population metadata. CI run38537399526found no A2S response over IPv4, IPv6 or mapped IPv4 even inside the container withIPV6_V6ONLY=false. Static inspection of the shipped THQNsteam_api64.dllidentifiesSteamNetworkingSockets_LibV12::CreateListenSocketIPat the configured query port. Neither that socket nor compiled-in RakNet support proves a metadata query API. Generic UDP silence establishes absence of an ICMP rejection, not an application response or measured RTT. Docker's TCP proxy is not a native TCP listener. Keep protocol probes diagnostic until a native response is verified; do not substitute a transport handshake for game state. Workshop acceptance additionally requires an exact registered event row under the latest nativeTracks:response, captured from the current disposable server lifecycle. - Current Squad uses
squad_rconfor health and info: authenticated nativeShowServerInfoandShowCurrentMapexchanges share one overall deadline. The parser exposes only public metadata, validates population bounds and requires a confirmed map/layer rather than trusting the JSON'sUnknownmap placeholder. The module owns local RCON configuration and port claims; no provider account is needed for this read-only health protocol. -
utils.source_console.build_source_container_start_commandrelays the runtime console FIFO throughbsdutilsscript so Garry's Mod and L4D2 see a real input terminal. Process commands keep their ordinary launch path. - Garry's Mod validates GMAD tables, paths, sizes and CRCs before extracting GMA payloads into owned addon trees. Unturned preserves existing File_IDs; Project Zomboid inspects multiplayer mod metadata and preserves unowned WorkshopItems/Mods entries.
-
download_workshop_itemsexposes the verified staged downloader to native adapters, checking anonymous capability, game identity and complete item metadata before SteamCMD. SteamCMD legacy success paths may name a file inside the expected content directory. L4D2 normalizes legacy.binuploads only after validating a VPK header. - Workshop download verification requires the item-specific SteamCMD success path, an expected app/item content directory under the requested cache or configured SteamCMD root, and nonempty file content. Exit code zero and an old cache directory are insufficient. Anonymous access errors remain errors.
-
plugin_jars.pypackages the repeated desired-state, checked-in manifest resolution, dependency expansion, direct URL handling for plugin payloads, Mod DB archive installs, and owned-file cleanup flow for plugin-style modules such as Paper, the proxy family, and TShock. -
source_addons.pypackages the repeated desired-state, manifest registry loading, archive staging, GameBanana/Mod DB/workshop intake, direct single-file URL handling, and owned-file cleanup flow for Source-engine games that install addons under a known game directory. - Thin Source wrappers that share the same
addons/layout can reuse one checked-in registry under src/gamemodules/source_curated_mods.json viaload_shared_source_curated_registry()instead of carrying identical MetaMod/SourceMod manifests per module. -
reconcile.pycompares desired vs installed state so modules can add missing curated entries and remove only files that AlphaGSM previously recorded as owned.
Modules that opt into curated mod support still own the game-specific layer:
- keep the canonical module import surface stable, even when the implementation is package-backed
- seed and persist the module-specific
modsdatastore shape - expose module commands such as
mod add ...,mod list, andmod apply - ship the curated registry file beside the module package and keep its destination rules aligned with the real game layout
- validate provider-specific desired entries such as curated families or workshop ids before saving them
- fail clearly and non-destructively when a provider is still experimental or not yet verified for apply-time installation
When a module supports more than one source class, keep the distinction explicit:
-
manifestorcuratedmeans AlphaGSM owns the catalog entry through a checked-in registry file and resolves a known family plus optional channel/version into a concrete release. - provider-backed sources such as
gamebanana,workshop, ormoddbmean the user supplied either an external item id or a canonical provider page URL and AlphaGSM resolves live metadata from that provider at apply time. - direct
urlsources mean the user supplied a concrete download URL and AlphaGSM still validates scheme, filename shape, destination roots, and owned-file cleanup through the shared mod-support layer. - Treat provider-id sources as shared integrations that can be reused across modules; do not bury provider-resolution code under one game package unless the upstream service is genuinely game-specific.
- Prefer documenting
manifestas the public user-facing term when the command surface needs to distinguish AlphaGSM-owned entries from external provider ids, but keepcuratedaccepted when older commands or datastore state already use that name. - Keep these source classes separate in the datastore so support expectations, reproducibility, and future update semantics stay clear.
-
configure(...)stores at minimumportanddirinserver.data, initialises the backup data structure, and returns(args, kwargs)to forward toinstall -
install(...)ensures the server filesystem is in a runnable state and callsserver.data.save() -
get_start_command(...)returns(argv_list, working_dir)
Use the provided script to statically validate gamemodules' get_start_command shape:
python3 scripts/validate_start_commands.pyThis script performs a conservative check that get_start_command returns a tuple
containing an argv-style list and a working-directory path (commonly server.data["dir"]).
If you need a simple inventory of modules missing get_start_command, run:
python3 scripts/list_missing_start_commands.py- schema-backed
setkeys should usesetting_schemaplusresolve_requested_key(...)in the sharedServer.setflow, withsync_server_config(server)handling the on-disk native config update for datastore keys inconfig_sync_keys - every maintained game module must define
import server.runtime as runtime_modulewhen it uses shared Docker builders, plus explicit module-scopeget_runtime_requirements(...)andget_container_spec(...)wrappers - choose the runtime family first, then call
server.runtimebuilders orutils.protonhelpers inside those wrappers; do not rely on runtime inference to add the hooks for you - Windows-binary modules that already use
utils.proton.wrap_command(...)should normally build Docker metadata throughutils.proton.get_runtime_requirements(...)andutils.proton.get_container_spec(...)so process and container modes stay aligned - when a process-runtime launch depends on a host-installed command, Java runtime, or shared library, prefer declaring it in
host_dependenciesthrough the module'sget_runtime_requirements(...)output instead of raising an ad hocprestart(...)error; the shared runtime gate now blocksscreen,tmux, and subprocess launches before start, renders platform-aware install guidance for Linux/macOS/Windows when provided, and recommends the Docker runtime as the fallback path - scope host dependencies with
platformswhen they only apply on one OS. For example, Linux-only headless wrappers that invokexvfb-runshould declare anxvfb-rundependency withplatforms=("linux",)so Windows/macOS local launches do not get a false Wine/Xvfb requirement - keep Linux headless wrapper metadata aligned with the real launcher surface. If a module or launch script invokes
xvfb-run, declare it inhost_dependencies;tests/unit_tests/test_runtime_contract_static.pynow audits that contract repo-wide - Linux-native containers that cannot run as root may opt into
run_as_host_userthrough both runtime wrappers. Keep the game command unchanged: the shared Docker runtime adds--user <effective UID>:<effective GID>, sets the declared absolutecontainer_home, and mounts a per-server HOME from<manager-root>/runtime/<server>/home. -
run_as_host_useris deliberately opt-in and rejects an effective UID of0. Do not add module-specificuseradd,chown,chmod, HOME-directory creation, or process-versus-Docker command branches; declare the requirement and letserver.runtimeenforce it. - Container wrappers may declare boolean
init: trueto request Docker's init process;doctor --jsonreports the resolved choice. TF2 uses it so its terminal relay does not adopt Steam crash-upload children and stall in util-linux's child-exit handling. - The
alphagsm-dockerwrapper derives the manager UID from Bash's read-only effective-process identity and the primary GID through a trusted absolute systemidexecutable, then reads the Docker supplementary GID from the socket. It does not trust callerPATHor identity variables. It reuses a running manager only when Docker reports that exact identity/group tuple and exactly one matching Docker socket mount source. Before recreating an old root, stale-group, or stale-socket manager through the active mode, it walks existing state without following symlinks and fails before mutation if that state is not writable/traversable by the new non-root identity. - Host-user HOME state must remain outside the writable game-content mount. The runtime normalizes mount targets, rejects duplicate or read-only HOME mounts, validates existing manager-state components without following symlinks, and creates missing components with directory-relative no-follow operations.
doctoruses the same non-mutating validation and reports whether first-run state is safely creatable. -
setnow preflights any claim-affecting datastore change before persistence, including top-levelport/*portkeys, hosted-IP keys (bindaddress,publicip,externalip,hostip), and nested runtimeportsedits -
setup(...)now resolves port ownership beforeinstall(...); it may auto-shift the whole claimed port group only for default-owned claims, and must preserve earlier explicit user intent across later setup runs -
start(...)now performs a strict port-manager preflight beforeprestart(...)and runtime launch
Static enforcement lives in tests/unit_tests/test_runtime_contract_static.py, which imports every game module and verifies the Docker runtime hooks are present in module scope.
- Backend runtime coverage is tracked separately in
tests/backend_integration_tests/docker_family_matrix.py. Keep three declared representative cases per runtime family, and keep the active cases green in CI throughtests/backend_integration_tests/test_backend_docker.py. - Cheap backend lifecycle coverage for the shared port-manager path lives in
tests/backend_integration_tests/test_backend_port_manager.py; keep it green in CI because it proves the cross-runtimecreate -> setup -> start -> query -> info -> stopcontract without heavyweight game downloads. - Integration tests should now prefer module-aware Docker selection instead of hard-coding
runtime_backend="docker"per file. Intests/integration_tests/conftest.py, callwrite_config(..., runtime_backend="auto", module_name="<canonical module id>")for container-capable modules so the harness uses the module's explicit Docker contract and branch-local runtime images by default, while still falling back to plain process mode for modules that do not declare container support. - Keep the integration config dual-wired:
write_config(...)now writes both[process]and[docker]sections on purpose. That lets Docker-first integration configs fall back cleanly if a test probes a non-container module, and it avoids breaking process-only servers when shared runtime-image work is in flight. -
checkvalue(...)delegates"backup"key paths tobackup_utils.checkdatavalue -
get_info_address(...)preventsServer.info()from falling back to the TCP ping last resort
- src/gamemodules/palworld/ — custom SteamCMD install composition
- src/gamemodules/hl2dmserver/ — Valve family plus explicit Docker ports
- src/gamemodules/minecraft/vanilla.py — Java family
- src/gamemodules/teamfortress2/ — Source family with curated addons
-
src/gamemodules/readyornotserver/ — example with
get_query_addressandget_info_address
Per-server state is persisted as JSON under the configured server datapath.
Primary files:
Typical persisted keys include:
moduleruntimeruntime_familydirportexe_nameurlversionjavapathimagejava_majorcontainer_namemountsenvportsport_claim_policynetwork_modestop_modebackup- Steam app metadata for Steam-backed servers
Port ownership is derived from:
- top-level
portand*portkeys - hosted-IP keys such as
bindaddress,publicip,externalip, andhostip - runtime/container
portsmetadata - local query/info hooks when they imply an additional claimed local port
Keys declared as secret=True in a module's setting_schema are stored in a separate
<name>.secrets.json file rather than the main <name>.json data file.
-
Server.__init__calls_configure_secret_split()immediately after the module is loaded. That method collects every storage key whoseSettingSpeccarriessecret=Trueand passes them toJSONDataStore.set_secret_keys(keys, secrets_path). - At save time the data store writes secret keys exclusively to
<name>.secrets.jsonusingos.open(…, O_WRONLY | O_CREAT | O_TRUNC, 0o600)so the file is never readable by other system users. It thenos.chmods the file to enforce0o600after write. - At load time the data store merges the secrets file back into the in-memory dict, so the rest of the code operates on a single unified namespace.
-
alphagsm data(which callsprettydump()) redacts any non-empty secret value as<redacted>rather than printing the plaintext. - If a module has no
setting_schema, or none of its schema entries carrysecret=True, no secrets file is created and the data store behaves exactly as before.
To mark a password or token field as secret in a new or existing module:
from server.settable_keys import SettingSpec
setting_schema = {
"adminpassword": SettingSpec(
canonical_key="adminpassword",
description="Server admin password.",
secret=True,
),
}Modules that only need secrecy and have no other schema requirements can provide a minimal
setting_schema containing only the password entries. Non-password set operations on
non-schema keys still fall through to checkvalue / str_keys unchanged.
Modules can declare get_asset_requirements(server) to opt into setup --assets PATH, assets import PATH, assets check [--json] and assets export SOURCE --output ZIP. server.assets owns command orchestration; utils.asset_import
owns portable staging, complete-input validation, hash ownership and rollback.
Recipes use forward-slash paths: roots selects allowed source roots, include
contains anchored allowlist globs, required lists nonempty source-relative
files, and target is a relative destination prefix beneath the install root.
case_insensitive=True normalizes source-relative filenames to lowercase.
Only immutable game assets belong in these recipes. Keep credentials, saves,
operator config and manager metadata out. Imports require a stopped server;
setup imports run after module configure and port resolution, before install.
ZIP exports exclude all files outside the allowlist and refuse output inside
the source. Ownership lives in .alphagsm/assets.json, independent of mod state.
Provider auth, exported settings and external services retain their own APIs.
Required-file checks are presence checks; upstream version/format checks and
real lifecycle evidence are still necessary. Project Reclaimer is the first
consumer; existing BYO modules have not been migrated automatically.
Reclaimer declares reclaimer_rcon for authenticated WebSocket status. Its
Docker spec uses the existing Wine family with read-only game assets, host
networking, stop_signal="SIGINT" and stop_timeout=30. Stop options are opt-in;
existing modules retain their defaults. Refer to the module guide for the
explicitly deferred real process/Docker validation and fixed-map limitations.
The download subsystem is split between:
- src/downloader/downloader.py cache lookup, lock handling, and shared-download ownership
- src/downloadermodules/url.py HTTP download and decompression
-
src/utils/steamcmd.py
SteamCMD bootstrap and Steam app installation.
App and Workshop installs retry only a state 0x202 flake (known app ids, or
any app after a zero-progress reconfigure), state 0x2 when the update job
leaves that app Update Required, Missing configuration for that app, or a
first-run
Steam Console Clientbootstrap that exits 0 withoutERROR!. Other SteamCMD failures raise on the first attempt.skip_for_known_steamcmd_issueis unchanged.set memoryandset cpusare Docker run limits, not game-config keys. A Java container with no-Xmxreceives a heap of 75 percent ofmemory.wipe --mode softdeletesget_wipe_paths.wipe --mode hardreinstalls throughinstalland restoresget_hard_wipe_preserve_pathsplus.alphagsm. Framework packs that setreplace_unownedcopy an existing game file to.alphagsm/mod-originals/before replacing it.mod cleanuprestores that copy.
AlphaGSM resolves a runtime per server:
-
processby default for the traditional local flow -
dockeronly when[runtime] backend = dockeris configured and the module exposes explicit runtime hooks
Within process, the concrete launcher is selected independently by [process] backend:
screentmuxsubprocess
Core helpers:
Modules with derived or protocol-specific port layouts should expose one
module-scope port_claim_definitions tuple and reuse it when building
get_runtime_requirements(server) and get_container_spec(server). Each
definition uses key and protocol, with an optional integer offset from
the datastore value named by key; for example, {"key": "port", "offset": 1, "protocol": "udp"} describes port + 1. The shared
server.runtime.build_port_specs(...) helper validates the derived host and
container ports are within 1..65535, and the port manager consumes the same
definitions for process and Docker ownership checks. Keep runtime selection
out of this game-module contract: a module declares what the server needs,
while the shared runtime layer decides how those ports are launched and
published.
Container image scaffolding currently lives under:
- docker/README.md
- docker/java/Dockerfile
- docker/quake-linux/Dockerfile
- docker/simple-tcp/Dockerfile
- docker/steamcmd-linux/Dockerfile
- docker/service-console/Dockerfile
- docker/wine-proton/Dockerfile
- docker/wine-proton/entrypoint.sh
The shared Ubuntu 24.04 runtime images intentionally carry a small set of
legacy compatibility packages for older dedicated-server binaries that still
link against removed distro libraries, currently including libssl1.1,
libssl1.0.0, and libprotobuf10. When one of these packages becomes a real
host requirement for process-backed users, document the matching install step
in README.md and the affected server guide under docs/servers/.
Lifecycle model:
- build command line
- resolve runtime metadata from the datastore and module hooks
- claim the module's complete port set before runtime-specific launch work
- for
process, writescreenrcif needed and start a detached session - for
docker, assembledocker runargs from the container spec - inject console commands through the selected runtime
- use
doctorto print the effective runtime decision and local runtime-health checks for a server
Integration tests live under tests/integration_tests. Run the repository target with:
make integration-testSmoke tests live under tests/smoke_tests. Run all smoke tests with:
make smoke-testThe canonical smoke runners are:
Run one runner through the Make target with, for example:
make smoke-test SMOKE_TEST=run_minecraft_vanilla.shThe smoke runners are the best repository examples of the real lifecycle a user should follow. They:
- create isolated temporary configs
- show the exact command sequence a real operator would use
- stream command output directly into CI logs
- verify readiness and shutdown using server-aware checks
gmodserver install behaviour:
- download common mountable Source content into
<install_dir>/_gmod_content/instead of the server root - write
garrysmod/cfg/mount.cfgentries forcstrike,hl2mp, andtf - seed
garrysmod/cfg/mountdepots.txtwith Facepunch's default depot list
For documentation changes, prefer the smoke tests over hand-written examples.
Lint is driven by lint.sh.
Properties of the current lint pipeline:
- enumerates maintained Python files under the primary source trees
- runs pylint through the selected interpreter
- enforces
--fail-under=10
Command:
bash ./lint.shTreat repeated integration failures as defects until evidence establishes an intermittent cause. Keep the original JUnit failure alongside the single isolated recheck. Retry transient downloads at the transfer boundary (three attempts for connection interruptions or HTTP 429/5xx); do not increase whole-lifecycle retry counts to hide startup crashes, missing libraries, or wrong query endpoints. Readiness timeouts and unexpected shutdown failures must fail smoke tests too. The shared smoke helpers retain query errors and capture runtime logs/doctor output before cleanup can remove the failed server.
Smoke runners return 77 only for an explicit disabled module or unavailable
provider/owned-asset/host prerequisite. Setup errors, download failures, disk
exhaustion, readiness timeouts, and shutdown failures remain failures. CI and
make smoke-test display prerequisite skips separately from passes. The smoke
jobs also publish their result records to the job summary before uploading them.
Test artifact uploads retry once after a transfer/finalization error; the final
attempt and the required summary gate still fail if evidence is missing.
GoldSrc and Source smoke runners poll info --json for an A2S response before query and
info; a console marker alone does not prove protocol readiness. Source smoke
fixtures disable empty-server hibernation in their temporary config. Keep the
upstream Source wrapper when available, because it initializes the library search
path needed by the raw engine binary. Palworld uses its own dedicated-listener
message as the smoke readiness marker.
Valve launcher discovery tries the installation's own candidates before recursive content matches; downloaded GMod mounts must not select a different game's working directory. Custom executable overrides retain priority.
runtime.read_server_logs(server, lines=None) returns retained console output
without printing it, using the active process or Docker runtime. TeamSpeak uses
this to recover first-start ServerQuery credentials, caching them in an
exclusively created file with mode 0600 for subsequent container launches.
Authenticated ts3 info, rather than TCP availability, gates readiness.
ServerQuery sends are spaced by 350ms, including login and quit. The initial
delay also separates sequential CLI sessions. This respects the default ten
commands per three seconds without changing the server's flood protection;
see the TS3 exporter implementation notes.
Quake Live uses Steam A2S on its game port. Its servercfg directory selects
fs_game; +exec receives only the filename. Managed config entries use native
set syntax and serverstartup selects both map and factory. The factory
defaults to ffa, as illustrated in the original id Software config.
See also the A2SQuery supported-game table.
The integration readiness helpers use monotonic deadlines and stop early only
after two consistent doctor reports confirm the runtime has exited. Failed or
unknown Docker inspection results do not count as an exit. On failure, bounded
Docker diagnostics include exit/OOM state, process wait channels, stdin targets
and selected Steam log tails; existing redaction applies before logging.
Diagnostics also report bounded local TCP listeners and UDP bindings, plus
allowlisted numeric port arguments to check launcher forwarding. Other process
arguments and all environment values are excluded. Inaccessible Steam directories
are recorded as unavailable without aborting the remaining evidence collection.
Failed send commands collect the same bounded runtime evidence as failed start
commands, covering servers that exit before the readiness polling loop begins.
Classic Soldat uses the fixed file-server request for logs/gamestat.txt on TCP
game port + 10. The soldat query protocol requires the transfer terminator and
valid player-count/map fields, caps replies at 64 KiB, and bounds the whole
exchange by one monotonic deadline. It exposes no raw file or player-list data.
solserver synchronizes [GAME] Logging and [NETWORK] Allow_Download together
with its managed game settings; see the maintained query implementation.
ARK: Survival Ascended uses authenticated Source RCON ListPlayers for
readiness and information. ASA's old Steam query port is deprecated; declare
and publish the RCON TCP port and keep the admin password secret. ASA does not
reliably send Source's normal multipart terminator, so the query helper collects
fragments through a short quiet window while enforcing one overall deadline and
a 1 MiB response limit.
Modules can declare port_claim_definitions for auxiliary native listeners that
must be claimed in both process and Docker runtimes. A port definition's
default applies only when its datastore key is absent; an explicit None
continues to omit the mapping. Project CARS uses this to retain its native
Steam authentication port for older records without mutating their datastore.
For Source readiness failures, CI tries query and info --json before debugger
attachment. This separates a missing console marker from an unresponsive engine.
ALPHAGSM_DIAGNOSTIC_IMAGE enables native stack capture using the already-loaded
integration image (which includes GDB). A temporary container joins only the
target's PID namespace, has no network, uses a read-only root, drops all
capabilities except SYS_PTRACE, and keeps the default seccomp profile. The game
container's permissions remain unchanged. GDB skips initialization files,
auto-loading and debuginfod; it prints stack frames without arguments or locals.
Each attachment is limited to eight seconds, with at most two Source engine
processes and thirty seconds for the debugger container. Cleanup removes the
temporary container even after a Docker CLI timeout. Capture briefly pauses the
failed engine; native queries therefore happen first. Existing redaction also
applies to debugger output, and diagnostic failures never replace test failures.
The current Source investigation compares run 34250522498:
CSS and GMod process lifecycles passed; Docker still timed out after loading
Steam libraries. GMod's corrected cwd is /srv/server. The Docker snapshots
show a sleeping engine main thread and successful HTTP connectivity checks,
but neither proves whether Steam initialization is blocked or the engine is
running without the expected marker. Missing appinfo caches and early
SteamAPI_Init warnings are observations, not established causes. The next CI
capture must identify the waiting function and whether native A2S responds
before changing libraries, networking or runtime permissions.
Keep ALPHAGSM_MINECRAFT_RELEASE_ID and ALPHAGSM_MINECRAFT_SERVER_URL
paired and preserve them when switching the CI runner user. The shared Minecraft
fixture helper uses those pins without consulting the moving latest release;
a partial pin fails explicitly. This keeps the fixture aligned with the CI Java
runtime. Real integration/game/backend acceptance runs in CI for PR #36; local
validation uses unit tests and static checks.
The GitHub Actions workflow is .github/workflows/unittest.yaml.
Manual dispatch accepts an optional game_test_paths input containing existing,
space-separated tests/integration_tests/test_*.py and
tests/smoke_tests/run_*.sh paths. This limits the game matrix for CI-only
diagnosis while retaining unit, lint, image and backend gates. Omit it for the
full fleet. PR, scheduled, merge-queue and reusable-run routing cannot be reduced
through this input. For example:
gh workflow run unittest.yaml --ref YOUR_BRANCH \
-f game_test_paths='tests/integration_tests/test_asrdserver.py tests/smoke_tests/run_asrdserver.sh'The workflow runs for pull requests targeting master, scheduled coverage,
merge groups, manual dispatch and reusable release validation. Its current gates are:
- dependency/build setup, lint, unit tests, coverage, and standalone binary acceptance on Linux x86-64/ARM64, Windows x86-64, and macOS Intel/Apple Silicon
- change-classified, partitioned Linux game smoke and integration matrices, with separate standard and heavy lanes
- branch-local integration, Java, SteamCMD Linux, and Wine/Proton image builds used by the relevant lifecycle jobs
- backend process smoke/integration coverage plus backend Docker integration
- representative Minecraft backend integration on Windows and macOS
- four native Windows unit cases, with exact passing JUnit evidence required by the final summary; Linux unit and coverage jobs deselect those cases
Smoke, integration and isolated recheck commands run through
scripts/run_ci_workshop_tests.py. It enables every public anonymous Workshop
opt-in in the supported CI set and installs the required-case pytest plugin.
Keep this wrapper inside the su command after switching to gsmuser, so the
login shell cannot drop the flags. A required Workshop skip is a failure;
unrelated provider-authentication and BYO skips keep their prerequisite gates.
Local runs retain explicit opt-ins for the large content downloads.
Workshop integration parametrization uses workshop_runtime_backends() to
collect exactly the explicit process or Docker matrix lane. An ambiguous
auto selection is rejected. ARK and Space Engineers therefore have explicit
process and Docker lanes alongside the other native Workshop cases. Without
an explicit backend, local collection includes both runtimes.
Ubuntu 24.04 is the full game-server lifecycle baseline. The Windows and macOS jobs currently validate representative Minecraft backend paths only; broader game-server lifecycle coverage on those platforms and on newer or other Linux distributions remains future work.
ASTRONEER's dedicated server registers the address that players use rather
than a loopback or Docker bridge address. Its Docker smoke and integration
tests therefore run in the configurable heavy lane. Provision that runner
with an externally routable UDP endpoint, set
ALPHAGSM_HEAVY_RUNNER_LABELS_JSON to its runner label array, and set the
ALPHAGSM_ASTRONEER_REGISTRATION_PUBLICIP repository variable to the endpoint's
real IPv4 address. The endpoint must route the test's managed game port to the
runner. This setting is intentionally not used by AlphaGSM's local
process/Docker query resolver.
There is also a documentation publishing workflow:
That workflow mirrors selected repository docs into the GitHub wiki by pushing to the wiki repository.
Main template:
Important environment variables:
ALPHAGSM_CONFIG_LOCATIONALPHAGSM_USERCONFIG_LOCATIONALPHAGSM_RUN_INTEGRATION-
ALPHAGSM_GITHUB_TOKENfor authenticated GitHub release metadata requests ALPHAGSM_DEBUG
The smoke tests rely on temporary config files and ALPHAGSM_CONFIG_LOCATION to isolate state per run.
GitHub Actions supplies ALPHAGSM_GITHUB_TOKEN to smoke and integration jobs
using the workflow's read-only GITHUB_TOKEN; local runs can omit it unless
they encounter GitHub's anonymous API rate limit.
Smoke, integration, and backend lifecycle tests must allocate game-port groups
through scripts/select_test_port.py. On Ubuntu 24.04 it reads
/proc/sys/net/ipv4/ip_local_port_range and excludes that whole range before
probing TCP and UDP availability inside the default 10000-29999 test pool.
Do not replace this with bind(..., 0):
SteamCMD and other network-heavy setup commands can consume a kernel-selected
ephemeral source port between setup and start, causing a false live-listener
collision at AlphaGSM's strict port preflight.
Linux integration jobs run AlphaGSM inside a GitHub job container and launch
game-server containers through the mounted host Docker socket. The shared
runtime inspects the current container's bind mounts and rewrites source paths
back to host-visible locations before docker run. Container detection uses
the exported HOSTNAME when available and falls back to
socket.gethostname() because the non-root su - gsmuser login shell may
reset the environment variable.
The same job container exposes the host Actions tool cache at /__t.
Integration shards clear that unused cache before running the game matrix so
large SteamCMD payloads and branch-local runtime images can coexist on the
hosted runner disk. Keep that cleanup scoped to the integration jobs; build,
lint, unit, and coverage jobs may still need the tool cache.
The integration job image is built from the exact branch-local
wine-proton runtime image selected by the same workflow. Keep that base-image
dependency and build argument intact: Docker can then reuse the running job
container's Wine, prefix, Mono, and Proton layers when AlphaGSM launches a
Windows game-server container. Building those stacks independently can exhaust
the hosted runner after large SteamCMD installs.
Documentation is now deliberately split by audience:
- README.md non-technical, copy-paste-first usage
- docs user-facing server guides
- DEVELOPERS.md technical architecture and maintenance notes
When behaviour changes:
- update smoke tests if the real workflow changed
- update server guides if user steps changed
- update
README.mdif first-run commands changed - update
DEVELOPERS.mdif architecture, CI, or maintenance expectations changed
The repository is the source of truth. The wiki is a published mirror generated from tracked docs by scripts/publish_wiki.sh.
Regenerate per-game set tables with:
PYTHONPATH=.:src python3 scripts/sync_server_guide_settings.pySteamCMD session profiles support explicit owned-file acquisition on Linux;
see docs/steamcmd-auth.md for their scope and validation
limits. Live Steam-auth and BYO lifecycle tests live in local_tests/ and run
through ./run_local_tests.sh. Disposable integration files default to the
checkout's .local-tests/ directory. Set ALPHAGSM_LOCAL_WORK_ROOT to put
that tree on another disk. SteamCMD uses steamcmd_path when set, and
otherwise the Steam client directory for Linux, macOS, or Windows.
pytest.ini excludes that directory
from normal collection, and CI must not invoke the script. Profile directories,
passwords, and tokens stay gitignored. When AlphaGSM launches a game container,
the private steam-auth/ directory is not a container mount, and a mount that
would expose it is rejected before docker run. Anonymous
SteamCMD SDK mounts are separate. The manager wrapper forwards ordinary
commands with docker compose exec -T. ./alphagsm-docker steam-auth login PROFILE USERNAME
uses exec -it when stdin is a terminal, and a non-terminal login exits before
compose exec. Local design notes under
docs/superpowers/ are gitignored and are not part of the published docs.
Repo-local automation guidance now lives in:
binary.yml is reused by PR validation and the
release workflow. It builds Linux x86-64/ARM64, macOS Intel/Apple Silicon, and
Windows x86-64 executables with pinned runtime/build requirements. Each CI job
copies the executable into an isolated home outside the checkout, exercises
bundled manifests/templates, and drives a Minecraft lifecycle through separate
AlphaGSM commands, including console input and verified shutdown. Linux x86-64
also exercises the artifact with the branch-local Java Docker image. Integration
and real game acceptance for this PR run in CI; local validation uses unit tests,
lint, workflow checks, and builds.
The build collects package resources and dynamic imports together. Factorio's
placeholder implementation raises NotImplementedError at import and remains
excluded; a build must not imply unsupported code is enabled. Frozen processes
use the current executable for internal bulk-command dispatch, supervision and
Windows update workers, without invoking a sibling Python script. Never add a
runtime dependency on the source checkout or the build environment.
Tag publication waits for the complete validation workflow and signed-artifact acceptance. Manual release dispatch validates and builds unsigned artifacts; it does not publish. Linux artifacts receive checksums and GitHub build-provenance attestations. Windows/macOS signing additionally requires these repository secrets before a tagged release can succeed:
-
ALPHAGSM_WINDOWS_SIGN_PFX,ALPHAGSM_WINDOWS_SIGN_PASSWORD: base64 PFX and password for Windows Authenticode signing. -
ALPHAGSM_MACOS_SIGN_PFX,ALPHAGSM_MACOS_SIGN_PASSWORD: base64 signing identity export and its password. -
ALPHAGSM_CODESIGN_IDENTITY: Developer ID Application identity passed into PyInstaller so nested Mach-O payloads are signed too. -
ALPHAGSM_APPLE_ID,ALPHAGSM_APPLE_TEAM_ID,ALPHAGSM_APPLE_PASSWORD: Apple notarization credentials.
Signing and notarization fail closed when credentials are absent or rejected. No credentials were configured during this change's read-only repository check; release certification remains an operator prerequisite. The ASTRONEER runner variables described above were also absent. Do not invent substitute public IPs or mark those jobs passed without the required provider environment.
Use summarize-tests as the stable required check in branch protection/merge
queue settings. It checks the exact expected artifact inventory and required
job outcomes, rejecting empty, missing, malformed or duplicate reports. Only
an initial test-failure exit code can be recovered by one matching successful
isolated recheck. The initial failure and its standard/heavy runner lane remain
visible. FLAKY RECOVERED is a temporary non-blocking classification, not proof
that a recurring failure has been fixed; track its failing node and log evidence.
New pushes cancel superseded runs for the same PR, keeping validation focused
on the latest commit. The latest run keeps its complete selected test matrix;
fail-fast: false prevents one failing game from canceling the other games.
The required summary uses always() to report all available outcomes.
Missing or malformed recheck collector output remains a blocking error and
retains the original required-job diagnostics.
Scheduled and merge-group runs exercise broad coverage beyond PR routing.
Local CLI commands acquire a per-server lock before constructing Server, so
load/mutate/save operations serialize across invocations. Interactive connect
does not retain that lock. Library callers that need a read-modify-write sequence
must use JSONDataStore.transaction(); a stale in-memory object followed by
save() alone cannot merge concurrent edits. utils.state_io owns portable
advisory locks and same-directory atomic writes. Persistent lock sidecars must
not be deleted during use because replacing an inode defeats ownership.
Public and secret datastore files retain their existing schema. A private
.pending journal makes a paired write recoverable after interruption; once the
journal is durable, recovery can finish the committed update even if the original
caller received an I/O error. This is not a general multi-server transaction.
simple_kv_config uses the same lock and atomic replacement for config edits.
The subprocess supervisor keeps an authenticated loopback endpoint, an owned
process group/job, and a bounded stdin queue. A successful send means queued for
delivery; a full queue fails explicitly. Windows Job Objects clean up descendants
when ownership ends. On Unix, an uncatchable supervisor death with a live orphan
retains diagnostic evidence and conservatively refuses a duplicate launch.
Docker exec-console uses a private FIFO and preserves attach input; custom
images using this console mode must supply sh, mkfifo, and cat. Existing
containers need an AlphaGSM stop/start to acquire the new launch wrapper.
src/server/capabilities.py is the shared view of runtime, provider, config-sync
and query declarations. src/server/module_capabilities.json is the generated
inventory consumed without importing game modules. Regenerate it with
scripts/generate_module_parity_report.py; do not infer operating-system or
architecture coverage when a module has no explicit evidence. Published support
states come from checked-in trackers and prerequisites, not the latest CI run.
They must not be presented as fresh cross-platform validation.
doctor --json emits schema version 1 with check IDs, categories, statuses,
runtime selection and capabilities. Failed checks return exit code 1; a stopped
server by itself is healthy. Diagnostic strings redact declared secret values,
sensitive argument values, credentials and URL query data. CI failure artifacts
contain reports and relevant log tails; operator-supplied game logs may still
contain information that the game itself chose to print.
Successful setup/update writes a private provenance record under
DATAPATH/.provenance/<server>.installation.json. It records available observed
Minecraft jar metadata/hashes and Steam build IDs; unsupported or unobservable
fields stay null. This records the installed payload and operation, not a full
server validation. Modules can expose get_installation_provenance(server) for
additional authoritative evidence through the shared recording path.
Modules can declare process_platforms and process_architectures, or implement
get_platform_requirements(server) when the selected version/build changes the
requirements. The hook returns metadata without downloading or changing state:
def get_platform_requirements(server):
return {
"process": {"platforms": ["linux"], "architectures": ["x86_64"]},
"docker": {"operating_system": "linux"},
}The process declarations describe the hosts on which this AlphaGSM integration
can run, including any explicitly supported wrapper. Existing
supported_platforms/supported_architectures are fallback declarations. Keep
these separate from host dependencies and provider/configuration checks, which
continue through their existing shared APIs. A missing declaration is unknown,
not a claim of support. TF2 now declares Linux process support because its
current installation/launcher uses srcds_run and Linux Steam client libraries;
this does not describe every upstream TF2 distribution.
setup and update check declared compatibility before installation. start checks it
before prestart and checks the final executable's ELF, PE, Mach-O or shebang
format immediately before launch. The launcher inspected is the one resolved in
the server's working directory. Explicit interpreter and Wine/Proton commands
can run their supported payloads; AlphaGSM does not silently add Wine, WSL or an
emulator. Undeclared CPU compatibility is not inferred from a successful format
check. Docker uses the daemon's reported OS, so a Windows desktop with a Linux
Docker daemon can run Linux containers. The six checked-in runtime families
require Linux containers; a custom image without an OS declaration stays unknown.
The generated capability inventory records normalized declarations, while
doctor --json reports the current host or daemon compatibility. Add module unit
coverage when introducing declarations and prove the supported lifecycle in CI.
The shared SteamCMD installation helper currently ships a Linux client and rejects non-Linux hosts before creating directories or downloading. A Docker game-runtime selection does not relocate the host install hook; use the Linux manager container for this provisioning path on other desktop operating systems.
When upgrading an existing source environment that installed the obsolete
crontab distribution, remove that distribution with
python -m pip uninstall crontab, then install requirements-runtime.txt again.
AlphaGSM uses the python-crontab distribution's CronTab API. A fresh binary or
fresh environment does not need this cleanup.
agent.contracts is the dependency-free closed version-one wire schema.
server.public_query contains the immutable public A2S_INFO/Java SLP registry,
uses bounded parsers in utils.query, and never constructs Server or falls back
to console/RCON/HTTP/TCP health. Central consumers must pin a reviewed commit
and exclude their own infrastructure before numeric connect.
The optional agent package keeps scoped credentials and consent under a
private POSIX directory outside game datastores. Policy and report credentials
are independent. The foreground manager opens one outbound WebSocket for
presence and job wakes, and falls back to HTTPS long polling when the socket
cannot open. It rechecks exact local bindings and policy under the normal CLI
lock, obtains final online authorization and persists executing before lifecycle
effects. A socket frame never skips those checks. Results are posted over HTTPS.
Outbound HTTPS and the agent WebSocket use a verified client context with a
minimum of TLS 1.2.
alphagsm agent run starts the foreground agent and does not install a
service. alphagsm agent install-service writes alphagsm.service for the
current user, enables it, and starts it. install-service --system writes
the same unit under /etc/systemd/system, enables it for boot, and starts
it as one operator account, never as root. Both execute the invoked program
with agent run. There is no separate alphagsm-agent program. The Linux
account lookup is imported only for a system install, so Windows commands do
not load grp or pwd. Neither command changes reporting, management,
publication or credentials. uninstall-service disables, stops and removes
the selected unit and leaves the enrollment in place. The tour and game
setup do not install it.
Journal stores durable results until acknowledgement; interrupted execution
reconciles unknown and never repeats effects. A global foreground-service lock
prevents competing pollers. No secrets are accepted as CLI arguments.
The official provider is https://alphagsm-api.sector-alpha.net for every
agent operation: enrollment redemption, binding registration, ownership-challenge
minting, POST stats, and the outbound execution protocol (policy acknowledgement,
the agent session socket, command polling, final authorization and results).
The agent does not ask which host to use. Sector Alpha verifies a challenge by
querying the server. Owners queue commands and view data on the website.
Transport sends every approved route to the one configured origin. It retains
one canonical API provider origin in local configuration, without redirects or
endpoint discovery. No new persisted secret/origin fields are introduced.
canonical_agent_origin() still maps exact official HTTPS website/API authorities
(default/443 port, case and root-slash variants) to the canonical API provider
origin. Config.load() and the next ordinary write retain this normalization,
preserving IDs, keys, bindings and permissions. Explicit custom origins, lookalikes
and non-default ports keep the exact configured single origin for every agent path.
Restart foreground/user services after upgrading. The website still accepts
enrollment and binding registration from older clients.
The generic origin syntax validator still requires HTTPS roots and normalizes an
optional trailing slash. agent.inventory reads
only owned current-user JSON datastores without constructing Server (which can
migrate secrets or save runtime metadata). It exports an allowlist of module,
query port, primary player port, name, capacity and networking hints. agent.network accepts only globally
routable numerical suggestions, uses bounded TLS for its fixed address lookup,
and sends no agent credentials. Explicit --host bypasses discovery.
agent bind TAG, agent bind '*' and agent setup preview and confirm exact
bindings before registration or config writes. The portal's authenticated
POST /v1/agent/bindings accepts only a selected enrolled scope and SHA256 of the
local tag; it derives installation ownership from that scoped credential and
returns an idempotent binding ID/epoch. Registration sends only the tag reference;
later reports contain approved display metadata, whose name may match the tag.
Datastore secrets stay local. Registration grants no actions or publication. Existing
bindings retain their approved values on repeat runs; capabilities require
separate local enablement. Games outside the public query registry remain private.
Version-one report metadata has optional connect_port: a strict integer from
1 through 65535 or null. Existing required port remains the query port; unknown
metadata fields still fail validation. Missing connect_port remains valid for
legacy reports/configurations. Omission preserves the receiver's existing player
port; explicit null clears it.
Both presence and value participate in the final publication approval check.
Inventory infers it from the primary local port, independently of queryport;
guided and explicit binding approve it for every
canonical game. --connect-port overrides the advertised player port, while a
Minecraft --port override sets both unless the player override is explicit.
The final guided preview shows host/IP and both ports before saving. Existing
bindings acquire the field only through rebinding; runtime metadata refresh
never infers or changes either approved port. Registration remains scope/ref
only, and endpoint metadata uses the opted-in report channel. This does not add
public query profiles, client launch schemes or Steam client IDs.
Deploy the receiver's updated shared contract before clients create/rebind
servers with this field; old closed-schema receivers reject it. Existing saved
bindings keep omitting it until a confirmed rebind, preserving their wire format.
Hostnames are scoped to bindings, not installations or numerical machine IPs. Different game tags may advertise different subdomains that resolve to the same IP; each retains its own game profile, player port, query port, expected-IP policy, publication consent and central ownership challenge. Single-server rebinding does not mutate siblings, and wildcard endpoint prompts preserve per-server hostnames on repeat runs. The agent neither provisions DNS nor routes traffic between processes; a hostname alone cannot multiplex a shared numerical port.
agent.preflight applies hostname checks to copied public report items immediately
before transport. Optional per-binding expected_public_ips is local policy only;
it never enters metadata, registration or wire schemas. Automatic expectations
read allowlisted local network hints and use one route/IP lookup per batch. Fresh
complete A/AAAA answers must all be global and match expected machine addresses.
agent.dns_lookup uses pinned dnspython async resolution with absolute lifetimes,
an eight-worker bound and a 30-second batch ceiling. Timeout or mismatch changes
only the outgoing publication flag; later repair can restore publication without
rewriting consent. Numeric/private reports keep their existing behavior.
After DNS, the service rechecks current publication, endpoint, expected IPs and provider relationship under the config lock, then sends with the current reporting key before releasing the lock. Report POSTs have a hard ten-second HTTPS budget, including DNS, TLS, headers and body. Expiry closes active sockets; a stalled system resolver cannot later connect or accumulate additional workers. A service instance retains its initial origin/installation relationship and must restart after local re-enrollment before sending any new relationship's credentials.
The explicit check-endpoint diagnostic additionally probes only pinned matched
addresses through the immutable native registry, retaining the original SLP
virtual host and sharing a five-second protocol budget. It does not run as a
publication renewal hook, avoiding NAT-loopback failures turning into publication
flapping. Neither local check grants central endpoint verification. Standalone
builds collect dns dynamic protocol modules as well as the agent package.
Trusted local runtime helpers run in separate process groups with bounded
state/operation deadlines. held_descriptor/inherited_state_lock hand the
existing datastore sidecar lock to the child after an inode check. The helper
marks the descriptor non-inheritable for further command children and never
unlocks the shared open-file description; the parent retains exclusion until
it receives a result or kills the timed-out helper group. Unknown may reflect
an effect already applied by an independent game/Docker daemon.
Run tests/smoke_tests/run_sector_alpha_agent.sh, the agent/public-query unit
sets, and ALPHAGSM_VALIDATE_AGENT=1 with Minecraft integration for each runtime.
Windows credential storage deliberately fails closed pending an ACL adapter.
Binary package collection, lint and coverage include agent. The service lives
in the separate alpha-platform-lite repository; it supplies PostgreSQL roles,
TLS transport round trips and worker isolation validation.
Credential generation is the management dispatch fence. Reporting authentication uses its independent active key, so management key rotation cannot interrupt reports. Local key replacement requires the previously enrolled capability and never changes bindings/opt-ins; management generations must strictly advance.
File-based BYO modules expose get_asset_requirements(server) and own a nearby
byo_assets.json (Minecraft family helpers use a prefixed local filename).
utils.byo_files.load_requirements resolves only declared non-secret path keys
from the instance. Provider credentials stay in provider metadata/auth storage.
Rich recipes declare module, description, requirements, sources,
managed_import, validation, and source guidance. Requirements have a relative
path and kind file, glob, directory, or archive-member; the latter
includes a relative archive member pattern or a nonempty member_any_of
list. member_case_insensitive controls archive member matching independently
of filesystem path casing; member separators are normalized before comparison.
any_of declares alternative paths and optional marks advisory content.
Files must be nonempty unless a requirement explicitly says otherwise. Checks reject symlinks and report
game_readiness: not-checked. Manual recipes use sentinel-only validation.
Managed recipes also supply the existing importer fields roots, include,
required, and target. Required patterns are ANDed; each required_any group
is ORed internally and ANDed with other groups. destination_names explicitly
preserves a native filename when case-insensitive source matching would otherwise
normalize it (for example COD4's fileSysCheck.cfg). inspect_files uses
native filesystem casing for installed and selected staged files. Acquisition
prechecks explicitly use source=True to honor source case_insensitive
matching before normalization. The importer validates
selected staged bytes against rich requirements before publication or export,
while preserving rollback and ownership hashes. It never infers that unrelated
files from a full Steam installation may be copied. Existing Reclaimer recipes
without rich requirements remain compatible.
ETS2/ATS manifests opt in with config_home_setting: configdir. Relative homes
retain installation-relative paths and existing ownership metadata. For an
absolute home, the loader separates that inspection root from relative export
filenames and marks the recipe manual-only; it never passes an absolute target
to the importer. Requirements/check work without extending publication rights
to that external directory.
A Steam source records id, kind: steamcmd, numeric app_id, explicit
platform, authentication: required or anonymous, optional public branch,
and human instructions/source links. Source identities are not successful
entitlement/download evidence. utils.byo_acquire.acquire downloads the app to
private temporary staging, validates declared content and imports only the
allowlist. Passwords are never recipe fields. Explicit profile selection calls
steamcmd.download(..., auth_profile=profile) using the session-only auth helper.
Session profiles keep their own runtime/home under the AlphaGSM state root. Owner-only permissions, trusted ancestor checks, link rejection and an inherited child-held lock protect their lifecycle. Interactive SteamCMD receives passwords and Guard directly; automated downloads disable prompts and sanitize errors. Local profile status is not proof of server-side authentication. See SteamCMD session profiles.
No live authenticated or CI integration validation is claimed for these helpers. Unit tests use synthetic game files and fake SteamCMD child processes, including permission, cancellation, interrupted-parent locks, staging cleanup, failed publication and exact native filename cases. Later local validation must use one operator-selected game at a time with a real entitled session.
The setup source options auth_profile, steam_dir and assets_source are
manager arguments, consumed before game configure/install hooks. Conflicting
explicit selectors fail before configure. File preparation runs after existing
host/port checks and before module.install, retaining stopped-instance and
transactional importer guards. Without a selected source or remembered profile,
existing module setup behavior stays unchanged.
steam_auth_profile stores a validated profile reference only. It is manager
metadata outside config_sync_keys and can be cleared with the shared set path.
An explicit local source overrides it for one invocation. A complete installed
file contract avoids a repeated full download; failures never trigger fallback.
utils.steam_library.find_installed_app reads bounded public KeyValues app and
library manifests, resolves only the declared AppID, and returns a source tree
for the existing importer. Explicit steamapps paths restrict discovery to one
library. Missing/ambiguous installs, unsafe metadata and installdir escapes
fail before import; auth files and Steam content are never modified.
BYO selection declarations should describe the complete runnable dedicated-server
payload, not only the minimum requirements sentinels. Keep all compatible map
and supporting resource sets from the declared source, including required game
scripts and native libraries. Keep content archives intact. Omit known
unrelated client engines/renderers during installation; never delete untracked
operator content while tightening an existing recipe. A campaign row is only
complete when the reproducible source-specific acquisition/install/import code
and selection regression tests are checked in.
utils.archive_install.sync_tree(...) and install_archive(...) accept optional
keyword-only skip_relative_files, matching exact extracted-root-relative file
paths. The default is empty for existing callers. Q3 declares its known loose
client engine exclusions in Q3_CLIENT_ONLY_ENGINE_FILES; it retains an
explicitly configured executable and never filters inside PK3 content archives.
These exclusions control source publication and do not delete existing operator
files during setup or updates.
The sequential BYO campaign records native port query, AlphaGSM status, info and shutdown outcomes separately for every game. Establish exact configured endpoints and transport before testing; validate real protocol responses and matching server properties. When a failure exposes module or shared-helper wiring defects, check in the fix with a regression and rerun the selected real lifecycle before closing that row. Keep Docker wrappers/config-sync contracts and record probe scope so host-local success does not imply Docker or external reachability. Authenticated CI integration remains deferred.