The helper's lists¶
The privileged helper that parks and wakes devices, sets the clock's time
source and starts and stops services lives in
hammunition-tray
(D-056, amended 2026-10-02). The device and service catalogs stay in this
engine, so hammunition hardware apply writes two data files the helper reads
instead of importing the engine. A third, per user, is written by the install of
a unit that runs as a user service.
An allow-list is data, never an argument. The helper takes a name, looks it up in these files, and refuses by name one they do not carry. Nothing an unprivileged caller supplies is ever a path, a unit or a command, and neither file ever carries a station value (a callsign, a grid square). A root-run helper reads only a regular file owned by root and not writable by group or other, in a directory that is the same, never following a symlink.
| File | Written by | Scope |
|---|---|---|
/etc/hammunition/devctl-devices.yaml |
hammunition hardware apply |
the devices the helper may park and wake |
/etc/hammunition/devctl-services.yaml |
hammunition hardware apply |
the system services the helper may start, stop, enable and disable |
~/.config/hammunition/devctl-services.yaml |
installing a catalog unit with a user_services block |
the operator's own services (not written by hardware apply: it is written by the install of a unit that runs as one, today gps-tether and rig-service, and removed by that unit's uninstall) |
Both system files are root-owned, mode 0644, and open with the header line
hardware apply
rewrites them whole when the catalog or the machine's time daemon changes, and
hardware unapply removes each only when it starts with that header. A file at
either path without the header, or one this account cannot read, refuses the
apply (exit 2) and is never overwritten. The check is made when the plan is
built and install -D replaces whatever is at the path when it runs, so a file
created between the two would be replaced: /etc/hammunition is root's, so only
root could do that.
devctl-devices.yaml¶
Every catalogued class or device whose manifest carries a power_control
block, sorted by name. Today that is five entries: the classes bluetooth-controller, camera, gps-receiver and wwan-modem (all usb_deauthorize), and the field laptop's dell-dw5930e (pci_runtime, schema-valid and refused). The excerpt below shows gps-receiver. The shape is
hammunition-tray's contract 1 (the contract page in that repository's docs directory); the tray's
helper reader loaded a file this exporter wrote with no complaint (run once,
by hand, against its branch).
version: 1
devices:
- name: "gps-receiver"
summary: "USB GNSS receivers — position for APRS and grid squares, and time for FT8"
method: "usb_deauthorize"
quiet: []
usb_ids:
- vendor: "1546"
product: "01a5"
- vendor: "1546"
product: "01a6"
# ... one entry per confirmed identifier
The excerpt is a real one, cut after the second identifier. Every string is
double-quoted, always: an identifier like 0003 or 1e10 is a string and never a
number, whatever the reader's YAML version.
| Key | Meaning |
|---|---|
version |
the file's shape version, 1. A reader refuses one it does not know, by name |
name |
the catalog name; what park NAME and state use |
summary |
the catalog entry's one line |
method |
usb_deauthorize (applies) or pci_runtime (schema-valid and refused, D-056) |
quiet |
the consumers to hush before a park and restore on a wake, in order: networkmanager_autoconnect is the one verb that exists |
usb_ids |
the confirmed identifiers only (an unconfirmed one names nothing, and the engine's own matcher skips it too) |
usb_ids[].vendor |
four lowercase hex digits |
usb_ids[].product |
four lowercase hex digits; absent when the entry matches a whole vendor |
usb_ids[].product_string |
present only for an identifier the catalog marks ambiguous (D-028) and has read a product string for |
What the helper does with it. An identifier matches a bus device on vendor
and, where there is one, product. Where product_string is present and the bus
reports a string too, they must be equal; when the bus reports none, the
identifier alone matches. This is the engine's own rule
(hammunition.hardware.detect.match_catalog), which compares a product string
only for an ambiguous identifier, carried in the file so the helper needs
nothing else. Whether a match is certain (the engine's ambiguous flag on a
match, and whether a string is distinctive across the catalog) changes nothing
about whether a device may be parked (D-056), so neither is exported. A device
matched this way is parkable only through usb_deauthorize; the sysfs guard
that limits a write to authorized and power/control under a USB or PCI node
is the helper's and does not depend on the file.
devctl-services.yaml¶
version: 1
services:
- name: gpsd
unit: gpsd.socket
scope: system
description: the GPS daemon (socket-activated)
- name: time
unit: ntpsec.service
scope: system
description: 'the clock: the daemon GPS time feeds'
- name: gps-resume
unit: hammunition-gps-resume.service
scope: system
description: re-adds the receiver to gpsd after sleep
| Key | Meaning |
|---|---|
name |
what hammunition services (start, stop, enable or disable NAME) and the tray pass to the helper |
unit |
the systemd unit the helper acts on; fixed argv, never taken from input |
scope |
system here; user in the per-user file |
description |
one line, shown beside the switch |
The time row follows the machine. ntpsec.service where ntpsec's daemon
and configuration are installed (the daemon GPS time disciplines, D-058);
chrony.service where only chrony is (D-072); and ntpsec.service when
neither is found, which the plan says in so many words and the helper reports as
not installed. systemd-timesyncd is never named: it cannot take time from a
GPS. A machine that gains a time daemon later needs hammunition hardware
apply again.
A unit that is not installed is still listed by the helper, with enabled:
not-found, so a front end can say "not installed" rather than hide the switch.
hammunition services is how to read the result.
Inspect and reverse¶
cat /etc/hammunition/devctl-devices.yaml /etc/hammunition/devctl-services.yaml
hammunition services
hammunition hardware state
hammunition hardware unapply
unapply removes both files by content and leaves /etc/hammunition, which
also holds time.yaml. Each write is recorded in the transaction log as a
devctl_export event (transaction-log.md), without the
files' contents.
What is measured and what is not¶
Tested against a temporary root: the shapes above, the byte-for-byte read-back,
the refusal of a foreign file, the removal by header. Run once by hand, not part
of the suite (the tray is not a dependency here): the tray's own reader
(hammunition_devctl.devices.load_devices and services.load_services, from its
devctl-helper branch) loaded a devices file written by this exporter from the
real catalog and a services file, with no note: eleven identifiers on
gps-receiver, three services. Not measured: the tray's helper reading the
files as installed under /etc/hammunition (hammunition-tray 0.5.0 is released and the tray units install its helper, but that has not been run as root on a real machine), hardware apply
on a real machine with the lists, and the check the helper makes on a root-read
file (a regular file, root-owned, not group- or other-writable, in a directory
that is the same) against what install -D -m 0644 leaves: the mode is right by
construction, /etc/hammunition being root's has not been looked at on a
machine.