A user could not get the VirtualHere plugin to use their VirtualHere client and asked, reasonably, where the logs were. There was no good answer, and the reason they were stuck turned out to be ours. **The runner could not see /tmp.** `punktfunk-scripting.service` set PrivateTmp=yes, which hands the unit a private tmpfs. But integrating with things already running on the box is the entire job of a plugin, and on Linux those talk over /tmp: VirtualHere's client IPC is the FIFO pair /tmp/vhclient + /tmp/vhclient_response, X11 is /tmp/.X11-unix. So the plugin launched the vendor binary happily and could then never reach the daemon behind it — while the same command worked perfectly in the operator's own shell, because that shell has the real /tmp. No config change could fix it, which is exactly the loop the report described. PrivateTmp is now off, with /tmp added to ReadWritePaths (which ProtectSystem=strict would otherwise make read-only). **Plugin logs now land in the console.** Plugins are not host child processes — the runner is a separate bun process that import()s each plugin in-process — so nothing they print passed through the host's tracing, and the console's Logs page could not show a single plugin line. The fallback was journalctl on Linux; on Windows the runner's scheduled task writes no log file at all, so a failing plugin was diagnosable only by stopping the task and re-running the runner by hand. Both mean shell access on the host box, which is what the console exists to avoid — and it left the one question a stuck user asks with no answer. So the runner now tees its output to POST /api/v1/plugins/logs, and those lines join the host's own ring under one cursor, targeted plugin:<name>. The console grows a Host/Plugins switch beside the level filter; an empty Plugins view says the thing that is actually usually wrong (the runner isn't running) rather than "adjust the filter". The shipper keeps stdout authoritative — journald and foreground output are unchanged whatever the host is doing — and is built so that logging can never hurt the thing being logged: it never throws into a caller, holds a bounded queue that drops oldest and then says how many, backs off when the host is away (a restart is normal), and re-sends a batch the host failed to take. Lines logged while a POST is in flight are kept, which cost one round to get right: the first version held its recursion guard across the await and silently dropped exactly the lines a busy plugin produces. Runner lines that report a failure (a refused unit file, a crashed plugin, a give-up) now go out at warn/error instead of all arriving as INFO, so the console's level filter means something for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
58 lines
3.3 KiB
Desktop File
58 lines
3.3 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.
|
|
#
|
|
# 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
|