# 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. # # OPT-IN — unlike punktfunk-web, the package does NOT auto-enable this: the runner does nothing until # you add scripts or install plugins. Turn it on once you have automation to run: # systemctl --user enable --now punktfunk-scripting # # 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