# 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