Skip to content

Manifest schema reference

Generated from src/hammunition/manifest/schema.py — the authority. This page cannot drift from it, because it is rendered from the model definitions themselves: field names, types, whether each is required, and the description= text the schema carries in place. The prose above each table is the model's own docstring, which is where the cross-field validators and the reasoning behind a refusal are explained — a table cannot render a validator, so those are treated as part of the reference and kept current in the source.

A manifest is strict: an unknown field is an error, not ignored. That is deliberate — a typo'd field name that silently did nothing is the bug this catches at load time (D-016).


PackageManifest

Field Type Required Description
name str yes
version str yes
summary str yes
categories list[str] yes Flat tags, D-003.
install list[InstallBlock] yes
depends list[str] no
provides list[str] no
conflicts_with_repo_package list[str] no
after list[str] no Ordering, not dependency.
requires_kernel list[Literal[ax25]] no Kernel subsystems the software cannot work without, checked against the running kernel at plan time. A machine whose kernel lacks one defers the unit in a profile and refuses it by name. Linux 7.1 removed AX.25 (merge 64edfa65, 2026-04-24); hammunition.kernel reads the module tree. The vocabulary is what has been measured.
requires_java int \| None no The lowest Java major version the software runs on, checked at plan time with java -version (D-037, amended 2026-10-02). default-jre-headless is a metapackage whose version says nothing about the Java major, so the floor is read from the java on this machine, never from the archive. A machine below it (or with no java, when this unit's depends would not install one that meets it) defers the unit in a profile and refuses it by name; nothing is fetched to meet it. Measured from the upstream's build file or its jar's class-file major, never copied from a README.
desktops list[Desktop] \| None no The desktops this unit is for, when it is for some and not others: kde, gnome, xfce, lxqt, lxde, mate, cinnamon. Omitted means any desktop, which is every unit that is not a panel applet or the like. Decided at plan time against the session files under /usr/share/xsessions and /usr/share/wayland-sessions (and the same under /usr/local/share), never XDG_CURRENT_DESKTOP (sudo drops it): a machine with no session for any listed desktop defers the unit in a profile and refuses it by name (D-060). The case it exists for is hammunition-tray, a Plasma applet whose .deb pulls plasma-workspace onto an Xfce machine.
desktop_alternative str \| None no The unit that does this job on the desktops this one is not for, named in the refusal and the deferral so the operator is told what to install instead. It must exist in the catalog, and its desktops must share none with this unit's (checked when the catalog loads). Requires desktops.
menu_title str \| None no What the desktop menu shows for the entry the engine generates when this unit ships none of its own: what it does, then the command in parentheses (Contest logger (tlf)). Defaults to the unit's name, which is fine for a name people know (gqrx) and not for one they do not (wwl). Ignored for a unit that ships its own desktop entries.
menu_submenu str \| None no Gather every desktop entry this unit ships into one submenu of this title, under the unit's first category only, instead of listing them beside everything else in every category it carries. For a unit that ships a toolkit: GNU Radio puts 21 entries into a submenu, and the SDR and Digital Modes menus were unreadable with them inline.
binaries list[Binary] no
installed_files list[str] no Files the build's own install rule puts under the prefix, as paths relative to it (lib/libacars-2.so.2, lib/pkgconfig/libacars-2.pc). Declared effects only: they are checked after the run (D-031) and are what lets a build with no executable be decided as already installed (D-051) or compared by update; the engine never copies or removes them. An executable belongs in binaries, not here.
launchers list[Launcher] no
service_endpoints list[ServiceEndpoint] no
apt_repos list[AptRepo] no
system_modifications list[SystemModification] no
config_files list[ConfigFile] no
user_services list[UserService] no systemd user services the engine renders from station values, enables, and reverses on uninstall (D-073 §6). Rendered into the operator's ~/.config/systemd/user/; deferred when a station value is missing, like config_files.
debconf_selections list[str] no debconf preseed lines applied BEFORE the apt install, so a package's postinst reads them instead of taking a default that needs an interactive answer. Each line is ' ', the debconf-set-selections format. The one measured need: wireshark, whose non-root capture is off by default and whose group and dumpcap capabilities are only created when wireshark-common/install-setuid is preseeded true (measured on Debian 13, 2026-09-01).
reconfigure_after list[str] no Packages to dpkg-reconfigure non-interactively AFTER the apt install. Paired with debconf_selections for the case where a postinst action depends on another package in the same transaction: wireshark-common's setcap of dumpcap needs libcap2-bin, and apt does not guarantee it is configured first, so the reconfigure re-runs the action once the whole transaction is settled (measured on Debian 13, 2026-09-01).
scope Literal[system, user] no (default system)
status Status no (default supported)
status_reason str \| None no
status_date date \| None no
status_verdict VerdictSource \| None no
retire_reason RetireReason \| None no
supersedes list[str] no
superseded_by str \| None no
recommended_default bool no (default True)
toolkit_risk list[ToolkitRisk] no
update UpdateBlock yes
documentation Documentation yes

Selector

Restricts an install block to some subset of targets.

An empty selector matches everything and acts as the default. Resolution is first-match-wins in list order, so defaults belong last.

Field Type Required Description
distro list[str] \| None no
distro_version list[str] \| None no
arch list[Arch] \| None no

InstallBlock

One (selector -> method) pair. The method itself varies, not just its argument — js8call is apt on Linux Mint 22.3 and a cmake build elsewhere.

Field Type Required Description
when Selector no
install AptInstall \| SourceInstall \| GitInstall \| BinaryInstall \| VenvInstall \| NodeInstall \| PipxInstall \| DataInstall \| RegionalDataInstall \| DemTilesInstall \| TopoQuadsInstall \| RegisterInstall \| DerivedDataInstall \| KiwixBooksInstall \| MwmRegionsInstall yes
build_depends list[str] no apt packages needed to BUILD only. Never reported as installed.
binaries list[Binary] \| None no This block's own build outputs, replacing the manifest's binaries wherever this block is the one that resolves. A prebuilt archive selected by arch can carry a different path per architecture -- rayhunter's zip has installer at the top level and one rayhunter-check under a per-platform directory -- and one manifest-level list cannot describe both. Omit the key to use the manifest's list; an empty list is refused, because it reads as an override to nothing.
note str \| None no

AptInstall

Field Type Required Description
method Literal[apt] no (default apt)
packages list[str] yes
install_recommends bool no (default True) Whether apt installs this unit's Recommends. The global default stays what every target distribution does -- Recommends are not suppressed catalog-wide -- and this is a per-unit opt-out for a package whose Recommends conflict with the target's desktop stack. Debian's morse Recommends pulseaudio, which Conflicts pipewire-alsa, so on a PipeWire desktop apt satisfies the transaction by removing the machine's audio routing and the plan refuses it (D-022, issue #61, measured on Parrot 7.3 + KDE 2026-09-12). Set false and this unit's packages are installed by a second apt-get install --no-install-recommends, simulated separately and disclosed in the plan (D-052); everything else in the transaction keeps apt's defaults.

SourceInstall

Build from a verified source archive.

Field Type Required Description
method Literal[source] no (default source)
source RemoteArtifact yes
build_system Literal[autotools, cmake, qmake, qmake6, make, custom] yes
configure_args list[str] no
build_args list[str] no
compiler_flags list[str] no e.g. -Wno-incompatible-pointer-types. Six AHRL units need these.
project_file str \| None no qmake .pro / cmake subdir. MSHV needs a different one per arch.
patches list[Patch] no
build_dir str \| None no
autoreconf bool no (default False) Run autoreconf -fi before configure -- for autotools projects shipped without a generated configure (git checkouts, mostly). kalibrate-rtl proved the need (source-build-gaps #3); the planner injects the autotools toolchain when set.
provides_install_target bool no (default True) False when the project's build system has no install rule. The backend then installs the manifest's binaries explicitly instead of running make install, which would fail. Requires binaries to be declared.
install_tree bool no (default False) Install the whole built/extracted tree to /share/hammunition/ instead of (or beside) named binaries. For software that reads settings, resources or data beside its executable -- MSHV, run-in-place trees (gaps #6/#8). Requires a launcher (or binaries) so the tree is reachable.
tree_marker str \| None no One file, relative to the installed tree, whose presence proves the tree is what the launcher expects: yaac's YAAC.jar, js8spotter's js8spotter.py. The effect check reads it back after the run; cp -aT exits 0 on any directory, so without it a tree unit ended verified: true with no check at all (issue #27). Required exactly when the block installs a tree.

GitInstall

Build from a pinned git revision. ref must be immutable.

Field Type Required Description
method Literal[git] no (default git)
repo str yes
ref str yes Commit SHA or tag. Never a branch name.
build_system Literal[autotools, cmake, qmake, qmake6, make, custom] yes
configure_args list[str] no
compiler_flags list[str] no
project_file str \| None no qmake .pro / cmake subdir, as for a source build.
build_args list[str] no
autoreconf bool no (default False) Run autoreconf -fi before configure -- for autotools projects shipped without a generated configure (git checkouts, mostly). kalibrate-rtl proved the need (source-build-gaps #3); the planner injects the autotools toolchain when set.
provides_install_target bool no (default True) False when the project's build system has no install rule. See SourceInstall for the full note.
install_tree bool no (default False) Install the whole built/extracted tree to /share/hammunition/ instead of (or beside) named binaries. For software that reads settings, resources or data beside its executable -- MSHV, run-in-place trees (gaps #6/#8). Requires a launcher (or binaries) so the tree is reachable.
patches list[Patch] no Unified diffs applied after the checkout and before the build, in order, exactly as a source block's. linbpq's makefile runs sudo setcap inside the build (#96); the patch that removes it is the first use.
tree_marker str \| None no One file, relative to the installed tree, whose presence proves the tree is what the launcher expects: yaac's YAAC.jar, js8spotter's js8spotter.py. The effect check reads it back after the run; cp -aT exits 0 on any directory, so without it a tree unit ended verified: true with no check at all (issue #27). Required exactly when the block installs a tree.
pin_review PinReview \| None no Required when ref is a commit SHA rather than a tag. D-024.
commit str \| None no For a tag ref: the commit it must resolve to. The pin check then refuses a re-cut tag instead of only recording what it resolved to. CoMaps' tag is the one Flathub, nixpkgs and the AUR build, at this commit (D-024, D-069).
submodules bool no (default False) Check out every submodule, recursively, at the superproject's gitlinks, shallow (git submodule update --init --recursive --depth 1, upstream CoMaps' own command), then refuse unless git submodule status --recursive shows each one at its gitlink. D-069.
build_python list[str] no Hash-pinned requirement lines for a Python the build needs, installed into a venv in the build directory with --require-hashes; prepare, configure and compile run with it first on the PATH. CoMaps' CMake refuses Debian's protobuf 4.x (D-069). Build-only: it is discarded with the build directory and never reaches the operator.
prepare PrepareStep \| None no An upstream script run in the tree before configure. D-069.
extra_files list[ExtraFile] no Files the install rule leaves out, installed after it. D-069.

BinaryInstall

Vendor .deb, archive, or prebuilt executable.

Field Type Required Description
method Literal[binary] no (default binary)
artifact RemoteArtifact yes
format Literal[deb, tarball, zip, executable, appimage] yes
placements list[Placement] no Files of the unpacked archive installed at absolute paths, as a .deb's file list would, for an archive upstream publishes before it publishes a package (hammunition-tray 0.5.0). Root-owned, printed one by one in the plan, removed on uninstall on the log's attribution.
placement_dirs list[str] no Directories under PLACEMENT_ROOTS that hold nothing but this unit's placements; uninstall removes them whole, and only when the log attributes a placement inside them.
devctl_helper DevctlHelper \| None no The tray's privileged device helper, installed from this archive.
deb_package str \| None no The control-file Package name a deb artifact installs, read from the .deb itself (dpkg-deb -f file.deb Package), never assumed from the filename — wsjtx-improved's vendor deb installs as wsjtx, and GridTracker2's filename casing matches nothing. Required for format: deb; it is what uninstall hands to apt-get remove and what status probes.
strip_components int no (default 0)
install_tree bool no (default False) Install the whole built/extracted tree to /share/hammunition/ instead of (or beside) named binaries. For software that reads settings, resources or data beside its executable -- MSHV, run-in-place trees (gaps #6/#8). Requires a launcher (or binaries) so the tree is reachable.
tree_marker str \| None no One file, relative to the installed tree, whose presence proves the tree is what the launcher expects: yaac's YAAC.jar, js8spotter's js8spotter.py. The effect check reads it back after the run; cp -aT exits 0 on any directory, so without it a tree unit ended verified: true with no check at all (issue #27). Required exactly when the block installs a tree.

VenvInstall

A per-user Python virtualenv, hash-pinned end to end.

requirements lines are requirements-file syntax and every package line must carry at least one --hash=sha256: — pip then runs with --require-hashes, which extends the demand to the whole dependency tree. That is CLAUDE.md's checksum rule applied to PyPI: apt packages are distribution-signed, a bare pip install name is neither signed nor pinned, and the difference is exactly what the rule exists for. Generate the lines with uv pip compile --universal --generate-hashes.

Environment-marker lines (; python_version >= "3.10") and blank or comment lines pass through untouched.

Field Type Required Description
method Literal[venv] no (default venv)
requirements list[str] yes
python str no (default >=3.11)
env dict[str, str] no Build-time environment for pip. Exists for one measured case: a project using setuptools-scm, installed from a hashed release archive, has no .git to read its version from and needs SETUPTOOLS_SCM_PRETEND_VERSION_FOR_ (nanovna-saver proved it, 2026-08-30). Never secrets -- the plan prints this.
payload RemoteArtifact \| None no A verified archive whose extracted tree installs to /share/hammunition/, for software that is a data tree run by a venv rather than a pip-installable package. The two-unit demand (source-build-gaps #9): radiosonde_auto_rx and supersdr. Launchers reach the venv with the {venv} placeholder.
payload_build_script str \| None no A script inside the verified payload tree, run with sh before the tree installs -- radiosonde_auto_rx compiles its C demodulators via auto_rx/build.sh. Requires payload; declare its toolchain in the block's build_depends.
tree_marker str \| None no One file, relative to the installed tree, whose presence proves the tree is what the launcher expects: yaac's YAAC.jar, js8spotter's js8spotter.py. The effect check reads it back after the run; cp -aT exits 0 on any directory, so without it a tree unit ended verified: true with no check at all (issue #27). Required exactly when the block installs a tree.
expose list[str] no Console-script names from the venv's bin/ to wrap onto the operator's PATH (~/.local/bin). A venv nobody can invoke installs nothing while reporting success.
licence str \| None no The terms the installed software is under, when they are not a licence the operator would assume (SPDX where one exists, else the publisher's own words). Printed on the plan line that installs the venv, before the confirmation, and stated, never adjudicated (D-021, D-033). Requires licence_url.
licence_url str \| None no Where those terms are stated, on the publisher's site. Requires licence.

NodeInstall

A Node.js application built from a verified source archive (D-037).

One measured user: openhamclock, a Node and Vite web application whose release publishes no binary. The build is npm ci -> npm run <build_script> -> npm prune --omit=dev, every step with lifecycle scripts ignored, and the pruned tree installs per-user under $XDG_DATA_HOME/hammunition/node/<name> with a wrapper on the operator's PATH that runs node <entry> from it.

What it costs and how it is disclosed: Node comes only from the distribution's nodejs/npm packages, never fetched, and the plan refuses when they are absent or older than node_min_version. npm ci fetches the dependency closure from registry.npmjs.org, each tarball verified against the sha512 in package-lock.json — which lives inside the sha256-verified archive, so the whole closure is transitively pinned from one manifest hash. Both facts are printed in the plan.

Field Type Required Description
method Literal[node] no (default node)
artifact RemoteArtifact yes The sha256-pinned source archive. Must contain package-lock.json.
node_min_version str yes The lowest Node.js version, as MAJOR.MINOR, the application (its bundler included) runs on; the plan refuses below it. Measured by running it, never read from engines alone: openhamclock's dependencies satisfy Vite's ^18 floor and its server still dies on Node 18 at start, because require() of an ES module needs 20.19 -- a minor, which is why this is not a major.
entry str yes The script node runs from the tree root, e.g. server.js.
build_script str \| None no (default build) The package.json script that produces the runtime tree (Vite's build). None for an application that runs from source unbuilt.
build_output str \| None no (default dist) A directory build_script must produce, checked after the build (D-031: an npm run build exiting 0 is not evidence it built). None only when build_script is None.
command str \| None no Name of the wrapper put on the operator's PATH (~/.local/bin). Defaults to the manifest name.
env dict[str, str] no Runtime environment baked into the wrapper, e.g. PORT. HOST is refused: the engine always binds a node application to 127.0.0.1 (D-037), and a manifest cannot widen that.
preserve list[str] no Files inside the installed tree a reinstall keeps: an application that writes its configuration beside itself (openhamclock's .env) would otherwise lose it on every update.
patches list[Patch] no Unified diffs applied to the unpacked tree before npm runs, the source backend's mechanism. First user: openhamclock 26.7.0 reads HOST and then listens on 0.0.0.0 anyway, so the loopback bind the engine sets is one line of upstream away from meaning nothing.

RemoteArtifact

A file fetched over the network. Verification is not optional.

Field Type Required Description
url str yes
sha256 str yes Mandatory. There is no unverified path.
signature_url str \| None no
signing_key_fingerprint str \| None no

Patch

An in-tree source edit. AHRL does these with sed; we declare them.

Field Type Required Description
file str yes
description str yes
unified_diff str \| None no

PinReview

When a commit pin was last looked at, and by whom. D-024.

A tag carries an upstream signal: someone decided that revision was worth naming. A commit SHA carries none — it is perfectly pinned and perfectly arbitrary. When a project stops tagging, pinning a commit is the right answer, but it moves a judgement upstream stopped making onto us, and an unreviewed commit pin from four years ago is the same failure as an abandoned tag pointed the other way.

So the judgement is recorded rather than implied. This is metadata about our decision, not about the software, which is why it lives beside the ref rather than in documentation.

Field Type Required Description
last_reviewed date yes
reviewed_by str yes Who looked. A name or handle, so the next reviewer knows who to ask.
basis Literal[distribution_pin, own_choice] yes Where the choice of commit came from. distribution_pin is strongly preferred and must name the distributions; own_choice requires saying what was checked and found nothing.
distributions list[str] no Distributions packaging this exact commit. Required for distribution_pin.
rationale str yes Why THIS commit rather than any other, and what was checked. 'HEAD at the time' is not a rationale; it is the absence of one.
cadence_days int no (default 180) How long this pin may stand before it must be looked at again.

Binary

Explicit build-output -> installed-name mapping.

This is what dissolves the wsjtx / wsjtx_improved rename dance: both builds emit wsjtx, so AHRL renames around them. Declaring install_as makes the collision impossible instead of choreographed.

Field Type Required Description
produced str yes Path the build emits, relative to the directory it builds in: cmake's out-of-tree build directory, the source tree for the rest.
install_as str yes Final name in the install prefix.

Launcher

A generated wrapper script. 14 AHRL units need one.

Field Type Required Description
name str yes The wrapper's filename in ~/.local/bin, so the launcher is also what a shell finds by that name -- ahead of /usr/bin. It therefore never takes the name of the command it runs, of a binary the manifest installs, or of anything on the system PATH or in the unit's apt file list: name it for what it does (rigctl-dummy, hackrf_info-check; issue #174).
exec str yes Command template. May reference {endpoint:NAME}. A line starting with hammunition runs the engine, written into the wrapper as the absolute path of the hammunition that generated it (issue #145).
title str \| None no What the desktop menu shows for this launcher. Defaults to name, which a file name rarely says well (rigctl-dummy). The convention is what it does, then the command in parentheses (D-054).
working_directory str \| None no
terminal bool no (default False)

ServiceEndpoint

A remote service the software talks to.

Exists because AHRL hardcodes -b hamclock.com:80 into four generated launchers, and that host was reported to stop serving in June 2026. A dead upstream must be repointable by editing the catalog, not the launchers.

Field Type Required Description
name str yes
default_url str yes
description str yes
user_configurable bool no (default True)
note str \| None no

SystemModification

Field Type Required Description
kind Literal[udev_rule, modprobe_blacklist, group_create, group_membership, foreign_arch, package_purge, apt_pin, file_shadow, file_capability] yes
description str yes
detail str yes
reversible bool yes
reverse_hint str \| None no
group str \| None no For group_membership: the group to add the operator to. Required there, and forbidden elsewhere.
binary str \| None no For file_capability: an installed binary's install_as name.
capabilities list[Literal[CAP_NET_ADMIN, CAP_NET_RAW, CAP_NET_BIND_SERVICE]] no For file_capability: the Linux capabilities set with permitted and effective flags, applied only after typed consent; --yes cannot satisfy it.

ConfigFile

Templated configuration written on the operator's behalf.

AX.25 forces this into 1.0: its install appends wl2k ${MYCALL} 1200 255 7 Winlink to /etc/ax25/axports.

Field Type Required Description
path str yes
template str yes May reference {station.callsign} etc.
mode str no (default 0644)
append bool no (default False)
backup_existing bool no (default True)
skip_if_present list[str] no Append only: regular expressions, matched per line against the file as it is when the step runs. If any line matches any of them the append is skipped and the outcome says which -- the idempotence and the no-duplicate rule of a file like axports, where a second port with the same name or callsign is an error. May reference {station.*}; a value is matched literally (escaped), never as a pattern.

AptRepo

Third-party apt source. Key pinning is mandatory. D-040.

name becomes two file names under /etc/apt and is restricted to what one can safely be. key_fingerprint is the primary key's, forty hex digits for a v4 key or sixty-four for a v6 one, spaces permitted; the engine computes the same thing from the file it fetches and refuses anything else. key_url is https: the fingerprint check is what makes the key trustworthy, but the transport still decides who can see the request.

when narrows the repository to some targets, with the same selector an install block uses; unset, it applies everywhere. It exists because a publisher may serve one tree per release under a different URI -- Kismet serves .../release/trixie and .../release/noble, each its own Release file -- and a manifest that declared both unconditionally would add a noble repository to a Debian 13 machine. A target no repository applies to gets no repository, and the unit falls to the ordinary "the archive does not offer it" path (D-039), deferred by name.

Field Type Required Description
name str yes
uri str yes
suites list[str] yes
components list[str] yes
key_url str yes
key_fingerprint str yes
rationale str yes Shown to the user before the repo is added.
when Selector no The targets this repository applies to; unset means every target.

ToolkitRisk

Standing exposure register. D-015.

The component list is derivable from build_depends; upstream port status and the date it was checked are not, which is the whole reason this exists.

Field Type Required Description
framework Literal[qt5, qt6, gtk2, gtk3, gtk4, wx3.0, wx3.2, mono] yes
upstream_port_status Literal[ported, in_progress, no_path, unknown] yes
checked date yes
note str \| None no

UpdateBlock

Field Type Required Description
probe UpdateProbe yes
strategy Literal[reinstall, apt_upgrade, rebuild, manual] no (default reinstall)
cadence_hint str \| None no

UpdateProbe

How to learn the upstream version. D-010.

label_file is the one probe an upstream told us about directly: YAAC publishes no tags and no releases, but its own Help > Check for Updates fetches a one-line text file and compares it to the compiled-in build label (issue #31, from the author). The catalog records that file as data so an engine can make the same comparison; one measured user, like pypi.

Field Type Required Description
method Literal[apt_policy, github_release, github_tags, binary_version, label_file, pypi, kiwix, comaps_maps, none] yes
repo str \| None no
command str \| None no
pattern str \| None no
url str \| None no For label_file: the plain-text file whose content is upstream's current version label, compared verbatim against version.
package str \| None no For pypi: the PyPI project name when it is not the unit's name. update --upstream asks pypi.org for it.

Documentation

Required by CLAUDE.md. A manifest without these cannot ship.

Field Type Required Description
what_it_does str yes
why_you_want_it str yes
prerequisites str \| None no
known_problems str \| None no
upstream_url str yes
upstream_support str \| None no

ProfileManifest

A named bundle of packages. Flat tags with overlap, D-003.

Field Type Required Description
name str yes
summary str yes
packages list[str] yes
stage Literal[1.0, post-1.0] no (default 1.0)
consent ConsentGate \| None no
documentation ProfileDocumentation yes
suggests_one_of list[SuggestionGroup] no

ProfileDocumentation

Required by CLAUDE.md for every profile.

Field Type Required Description
what_it_installs str yes
why_together str yes
deliberately_excludes str yes
manual_configuration str yes
disk_footprint_hint str \| None no
who_for str \| None no Who installs this, in a sentence or two.
hardware_assumed str \| None no What hardware the profile assumes, or says it needs none.
footprint_short str \| None no Disk footprint in a few words, for the index table.
excludes_short str \| None no What it leaves out, in a phrase, for the index table.
goals list[str] no Goals in an operator's words ('Make FT8 contacts'). The profiles index inverts these into its 'which profile do I want' table, so the same wording on two profiles puts both on one row.
first_ten_minutes list[str] no Ordered steps for the ten minutes after install, Markdown.

SuggestionGroup

One-of-several optional companions, offered only when nothing serves.

Born from the claws-mail decision (Q-015 #1, resolved 2026-08-30): the EMCOMM stack wants a local mail client, but choosing one for the operator is desktop-distribution work — so the engine detects first (any of detect_commands on PATH means the need is already met and the system's own choice is respected), and only when nothing is found does an interactive run offer options, every one an open-source catalog manifest. --yes and non-interactive runs skip with a note, never block — the station-prompt precedent (D-035).

Field Type Required Description
name str yes
reason str yes Why the profile wants one, shown at the prompt.
detect_commands list[str] yes Binaries whose presence means the need is already met.
options list[str] yes Catalog manifests to offer, in display order.
recommended str \| None no One of options, flagged at the prompt as the suggested pick.

ConsentGate

An affirmative opt-in that --yes cannot supply. D-021.

The gate discloses a capability and asks the operator to affirm they have the authorization they need. It does not decide for them in either direction — neither granting permission nor refusing on their behalf.

Field Type Required Description
risk_categories list[RiskCategory] yes
env_var str yes Scripted path. Separate from --yes, and recorded when used.
disclosure str yes What the software can do. Capability, never legality.
affirmation str yes The question. Must ask about the operator's authorization.

ConverterTool

A program a converter runs that no archive packages, pinned. D-067.

The catalog supplies where it is and what it hashes to; the engine owns how it is run, exactly as it owns every converter's command line. It is fetched, verified against artifact.sha256, checked against size, and installed under <prefix>/share/hammunition/<unit>/ -- not under the unit's data directory, which holds only files that are read, never run (D-049). A signature_url is recorded and not verified; the fetch step says so, in the words every declared-but-unverified signature gets.

Field Type Required Description
artifact RemoteArtifact yes
size int yes Bytes, measured; a download of another size is refused.
licence str yes SPDX identifier where one exists, else the publisher's own words.
licence_url str yes Where the licence is stated, on the publisher's site.

DataArtifact

One file of an offline dataset: a map tileset, a Wikipedia ZIM, cty.dat.

size is declared so the plan can print it before the confirmation (D-049): a 1.33 GB Geofabrik extract on a field connection is a decision, and the number belongs in front of the operator, not in the download's progress bar. The fetch verifies the declared size against the bytes it received, so a wrong declaration is a refused manifest rather than a surprise.

Field Type Required Description
url str yes
sha256 str yes Mandatory. There is no unverified path.
signature_url str \| None no
signing_key_fingerprint str \| None no
size int yes Bytes, as published. Printed in the plan, verified on fetch.
format Literal[file, zip, tarball] no (default file)
install_as str \| None no For format: file, the name the file is installed under inside the unit's data directory. Archives extract their members and take none.
members list[str] \| None no For an archive: extract only these paths, as the archive names them (its top directory included); one ending in / takes everything below it. A member that matches nothing refuses the install. Without it the whole archive is extracted (D-071).
into str \| None no For an archive: the subdirectory of the unit's data directory it is extracted into, one plain name. Required on every archive of a unit with more than one archive, or with files beside an archive: an archive replaces the directory it is extracted into (D-071).

DataInstall

Offline data whose payload is the point (D-049).

Not software: the engine never executes what it installs here. The files land under <prefix>/share/hammunition/data/<name>/ and the reader -- kiwix, mbtileserver, a logger reading cty.dat -- names the data unit in its own depends. Every artifact is pinned and hashed like any other fetch; nothing is mirrored.

licence and licence_url are disclosed in the plan beside the size, because a dataset under ODbL or CC BY-SA carries obligations the engine states and does not adjudicate (D-021).

Field Type Required Description
method Literal[data] no (default data)
artifacts list[DataArtifact] yes
licence str yes SPDX identifier where one exists, else the publisher's own words.
licence_url str yes Where the licence is stated, on the publisher's site.

DemTilesInstall

Elevation tiles for the squares the station's map regions cover (D-061).

Like RegionalDataInstall, nothing is pinned in the manifest: which tiles are needed follows the operator's regions in station config, and each tile is resolved at plan time and verified by a sha256 the catalog carries (catalog/data/copernicus-glo30-pins.yaml) or by the MD5 in the publisher's object metadata, the plan saying which, tile by tile. provider is an enum so another source is a new member the engine implements, never a URL in the catalog: usgs-3dep (D-068, amended 2026-10-01) is USGS 3DEP 1/3-arc-second bare earth, chosen by the station's dem_source and checked by the S3 ETag its publisher lists.

Field Type Required Description
method Literal[dem-tiles] no (default dem-tiles)
provider Literal[copernicus-glo30, usgs-3dep] no (default copernicus-glo30)
licence str yes SPDX identifier where one exists, else the publisher's own words.
licence_url str yes Where the licence is stated, on the publisher's site.

DerivedDataInstall

Data produced by running a converter over another catalog unit's data.

converter names the transformation by enum, never a command line -- the catalog stays pure data (CLAUDE.md's founding invariant) and the engine owns what each enum member means. source names the catalog package whose data this is derived from; the manifest's own validator requires it to also appear in depends, so the plan always installs the source data before running the converter over it. Which install method source must actually be is CONVERTER_SOURCE_METHOD, checked catalog- wide because only the catalog knows what source resolves to (D-061).

Field Type Required Description
method Literal[derived] no (default derived)
converter Literal[navit-maptool, mkgmap, routino-planetsplitter, gdal-dem, brouter-mapcreator, mapsforge-map, mapsforge-poi, ustopo-mosaic, tilemaker-pmtiles, graphhopper-import, splat-sdf] yes The transformation to run. Each needs a source of one particular install method (CONVERTER_SOURCE_METHOD, checked catalog-wide, D-061): navit-maptool, mkgmap, routino-planetsplitter, brouter-mapcreator, mapsforge-map, mapsforge-poi, tilemaker-pmtiles and graphhopper-import need an osm-regions source; gdal-dem and splat-sdf (D-061, amended 2026-10-02) need a dem-tiles source; ustopo-mosaic needs a topo-quads source (D-068).
source str yes The catalog package name this is derived from: an osm-regions unit, for gdal-dem and splat-sdf a dem-tiles unit, for ustopo-mosaic a topo-quads unit.
boundaries str \| None no The catalog data unit holding country boundaries (one GeoJSON file) that navit-maptool merges into each region before conversion, so maptool files towns under a country and address search finds them (D-057 amendment, 2026-09-28). Must also be in depends.
program str \| None no brouter-mapcreator and graphhopper-import only, and required on both: the binary unit whose installed tree holds the jar the converter runs, BRouter's with its map creator (D-063) or GraphHopper's (D-076). Must also be in depends.
profiles str \| None no brouter-mapcreator only, and required there (D-063): the data unit holding all.brf and softaccess.brf, the map creator's filters, which BRouter's release zip does not carry. Must also be in depends.
elevation str \| None no brouter-mapcreator only, optional (D-063): the dem-tiles unit whose installed tiles are folded into the routing files as elevation. Without it the routes are flat. Must also be in depends.
alternative str \| None no gdal-dem and splat-sdf only, optional (D-068, amended 2026-10-01; D-061, amended 2026-10-02): the dem-tiles unit of provider usgs-3dep drawn from instead of source when the station's dem_source is 3dep. Must also be in depends.
fstopo str \| None no ustopo-mosaic only, optional (D-068, amended 2026-10-01): the topo-quads unit of provider usfs-fstopo whose sheets, when installed, are made a second QMapShack map, FSTopo.vrt, beside ustopo.vrt. Read, never depended on: the Forest Service publishes no checksum, so its sheets are installed only by name (CLAUDE.md's checksum rule), and US Topo's map works without them.
kit str \| None no tilemaker-pmtiles only, and required there (D-071): the data unit holding tilemaker's OpenMapTiles profile (config and Lua) and the Natural Earth shapefiles the profile names. Must also be in depends.
licence str yes SPDX identifier where one exists, else the publisher's own words.
licence_url str yes Where the licence is stated, on the publisher's site.
tool ConverterTool \| None no The pinned program the converter runs, for a converter in CONVERTERS_WITH_TOOL (mapsforge-poi: Maven Central's mapsforge-poi-writer, which no archive packages, D-067). Required for those converters and refused for every other.

DevctlHelper

The privileged device helper that ships inside hammunition-tray's archive.

A block that names an interpreter nobody wrote down. The helper is root code behind one polkit action (D-056); the engine copies it from the unpacked archive to the paths the tray's contract fixes, bakes the engine's own venv interpreter into the wrapper (the helper's time verbs import the engine), and writes the tray's polkit action. None of those paths or that interpreter is catalog data, so the block carries only what varies by pin: where the code sits in the archive and which modules it has.

Field Type Required Description
source str no (default devctl) The directory in the archive holding the hammunition-devctl entry script and the hammunition_devctl/ package.
modules list[str] yes Every module of the package, by file name. Explicit, so the plan lists exactly the files root will run, and a test compares it with the pinned archive.
min_contract int no (default 1) The contract number (hammunition-devctl --version) an already-installed helper must answer for the engine to leave it alone.

ExtraArtifact

A pinned file installed beside a build, with its size (D-069).

Field Type Required Description
url str yes
sha256 str yes Mandatory. There is no unverified path.
signature_url str \| None no
signing_key_fingerprint str \| None no
size int yes Bytes, as published. Printed in the plan, checked on fetch.

ExtraFile

A file a build's install rule leaves out, installed after it (D-069).

Either a pinned artifact or a file from the built tree, installed at <prefix>/<install_as> with mode 0644, after an rm -f so a symlink at the destination is replaced and never written through. CoMaps' install rule skips World.mwm and WorldCoasts.mwm when the tree has none (they are downloaded, not built), and leaves out categories_brands.txt; Flathub's manifest installs all three by hand.

Field Type Required Description
artifact ExtraArtifact \| None no
from_tree str \| None no A file in the checked-out tree, relative to it.
install_as str yes Relative to the prefix, under share/.

KiwixBooksInstall

The Kiwix books the operator chose in station config (D-066).

Like DemTilesInstall, nothing is pinned in the manifest: which books follows reference_books in station config, and each book resolves at plan time to a pin in catalog/data/kiwix-pins.yaml, generated from Kiwix's own .meta4 files. There is no licence here because the books do not share one: each book's licence line is in the hand-written catalog/data/kiwix-books.yaml, and the plan prints it beside the book's size before the confirmation (D-049 rule 2). provider is an enum, as dem-tiles' is, so another library is a new member the engine implements, never a URL in the catalog.

Field Type Required Description
method Literal[kiwix-books] no (default kiwix-books)
provider Literal[kiwix] no (default kiwix)

MwmRegionsInstall

CoMaps' own map files for the station's map regions (D-069).

Like DemTilesInstall, nothing is pinned in the manifest: which maps follows the operator's regions in station config, through the region table in catalog/data/comaps-pins.yaml, generated from CoMaps' map index at the commit the comaps unit pins. Each map is checked against that index's SHA-1 and exact size, the publisher's own check, and the plan says so. provider is an enum, so Organic Maps' CDN would be a new member the engine implements, never a URL in the catalog.

Field Type Required Description
method Literal[mwm-regions] no (default mwm-regions)
provider Literal[comaps] no (default comaps)
licence str yes SPDX identifier where one exists, else the publisher's own words.
licence_url str yes Where the licence is stated, on the publisher's site.

PipxInstall

Field Type Required Description
method Literal[pipx] no (default pipx)
spec str yes
system_site_packages bool no (default False)

Placement

One file of an unpacked archive, installed at an absolute path.

The same shape as a .deb's file list: a source inside the tree, a destination, a mode. Never a command, never a glob -- every file is named, so the plan prints each one and a file upstream adds later is not installed by surprise (a test compares the list with the pinned archive).

Field Type Required Description
source str yes A file inside the unpacked tree, relative to its root.
dest str yes Where it is installed: an absolute path under one of PLACEMENT_ROOTS, with a path component named for this project. Under /usr/local/ it follows the engine's prefix.
mode str no (default 0644)

PrepareStep

An upstream script run in the checked-out tree before configure (D-069).

CoMaps' configure.sh generates the symbols, drawing rules and strings the CMake build reads, and builds a helper tool to do it. The script is upstream's, named by path, never a command line the catalog writes; the engine owns how it runs. produces is what makes it checkable: CoMaps' generate_symbols.sh exits 0 with no symbols when optipng is missing, so a script's exit status is not evidence of anything (D-031).

Field Type Required Description
script str yes Path of the script, relative to the tree; run as ./