WP6.1 of design/library-scanner-plugins-implementation-plan.md. The library is a flagship surface and cannot depend on an opt-in subsystem (design D9, closing G9): once the scanners are plugins, a host whose runner is off comes up with an empty library and no obvious reason why. The security posture for on-by-default was already built and shipped — LocalService on Windows, a sandboxed systemd --user unit on Linux, the scoped plugin-token lane. Windows (.iss): the PunktfunkScripting task is registered ENABLED and started on a FRESH install, and left to the existing restore path on an upgrade. The distinction is a new TaskExists probe taken before StopBunRuntimes disables anything — TaskEnabled alone cannot tell a fresh install from an operator who deliberately turned the runner off, and defaulting to "on" would silently switch it back on for them. deb/rpm: `systemctl --global enable` from the postinst/%post, guarded to first install only so an upgrade never undoes a mask. `--global` because a maintainer script has no user session to act on, and it is the only mechanism that makes a --user unit on-by-default for everyone. sysext: RPM scriptlets never run from a sysext image, so the enablement symlink is baked in directly (/usr/lib/systemd/user/default.target.wants/). Without it the runner would ship present-but-off on exactly the platform where an operator is least likely to go looking for it. Opt-out throughout is `systemctl --user mask punktfunk-scripting` — `mask`, not `disable`, since a plain disable cannot remove a symlink under /etc or /usr. The unit comment, both package descriptions, and the docs-site plugins page all say so; the page also gains the Windows equivalent. Not gated on hardware: none of this is verifiable from a Mac. The .iss change needs an installer run (fresh + upgrade, and an upgrade with the task deliberately disabled), and the deb/rpm/sysext changes need a package build.
65 lines
3.8 KiB
Desktop File
65 lines
3.8 KiB
Desktop File
# punktfunk plugin/script runner — systemd USER unit (bun runtime, OPT-IN).
|
|
#
|
|
# Runs the operator's automation under supervision: loose files in ~/.config/punktfunk/scripts/ and
|
|
# installed `punktfunk-plugin-*` packages under ~/.config/punktfunk/plugins/. Each unit is an Effect
|
|
# fiber (a plugin restarts on failure with capped-jittered backoff; a bare script is one-shot).
|
|
# SIGTERM interrupts the whole tree STRUCTURALLY, so every plugin's scoped finalizers run before
|
|
# exit (clean deregister / preset release) — hence the generous stop timeout below.
|
|
#
|
|
# ON BY DEFAULT — the packages enable this for every user (`systemctl --global enable` from the
|
|
# .deb/.rpm scriptlets; a baked-in default.target.wants symlink in the sysext image). It used to be
|
|
# opt-in, on the reasoning that the runner does nothing until you add scripts or plugins. That
|
|
# stopped being true when the game-library scanners became plugins: the library is a flagship
|
|
# surface, and a host whose runner is off now comes up with an empty library and no obvious reason
|
|
# why (design/library-scanner-plugins.md D9).
|
|
#
|
|
# It remains opt-OUT, per user:
|
|
# systemctl --user mask punktfunk-scripting
|
|
# (`mask`, not `disable` — a plain disable cannot remove a symlink that lives in /etc or /usr.)
|
|
#
|
|
# Auto-wired like the console: a plugin's connect() reads the host's SCOPED plugin token + identity
|
|
# cert from ~/.config/punktfunk/{plugin-token,cert.pem} (written by the host's `serve`) — no env
|
|
# editing. The plugin token authorizes the plugin surface but not hook registration or pairing
|
|
# administration; a script that needs the admin surface sets PUNKTFUNK_MGMT_TOKEN explicitly.
|
|
[Unit]
|
|
Description=punktfunk plugin/script runner
|
|
Documentation=https://git.unom.io/unom/punktfunk
|
|
# Plugins talk to the host's loopback mgmt API; order after it. Soft (ordering only, no Requires):
|
|
# the runner supervises each unit with backoff, so a plugin started before the host simply retries.
|
|
After=punktfunk-host.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/punktfunk-scripting
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
# Deliver the stop signal to the runner process itself (it orchestrates the structural shutdown of
|
|
# its unit fibers), and give it room to run their finalizers before the cgroup is reaped.
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
TimeoutStopSec=30
|
|
# Sandbox: free hardening for well-behaved plugins. The filesystem is read-only outside the home
|
|
# directory (ReadWritePaths keeps plugin state, download dirs, and ~/.config/punktfunk writable);
|
|
# no setuid re-escalation; sockets limited to what automation actually uses (loopback mgmt API,
|
|
# LAN/IPv6 webhooks, unix sockets). A plugin that must write OUTSIDE $HOME (e.g. a library on
|
|
# another mount) gets a drop-in:
|
|
# systemctl --user edit punktfunk-scripting → [Service]\nReadWritePaths=/mnt/games
|
|
# NOTE: the mount-namespace options (ProtectSystem) need unprivileged user namespaces for a
|
|
# *user* unit; on kernels/distros that restrict those, drop them via the same drop-in.
|
|
#
|
|
# PrivateTmp is deliberately OFF (field report 2026-08-03, the VirtualHere plugin). A plugin's
|
|
# whole job is integrating with things already running on this box, and on Linux those talk over
|
|
# /tmp: VirtualHere's client IPC is the FIFO pair /tmp/vhclient + /tmp/vhclient_response, and X11
|
|
# is /tmp/.X11-unix. A private /tmp namespace hides all of it — the plugin launches the vendor
|
|
# binary fine and then cannot reach the daemon behind it, which presents as an unexplained error
|
|
# that no amount of config fixes (the operator's own shell works, because that has the real /tmp).
|
|
# ReadWritePaths=/tmp puts the write bit back that ProtectSystem=strict takes away.
|
|
NoNewPrivileges=yes
|
|
PrivateTmp=no
|
|
ProtectSystem=strict
|
|
ReadWritePaths=%h /tmp
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
|
|
[Install]
|
|
WantedBy=default.target
|