Skip to content

v1.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Oct 14:33
· 4 commits to main since this release
748157a

report, install and watch stop saying what was not so, and docker_top and config validate stop handing an agent a token from a container's command line or a webhook URL.

Each half has an edge. homebutler --server X docker top --json returns full command lines until that remote is upgraded, and the table docker top prints on your own terminal still shows them. config validate cannot tell a token written where the host goes from a hostname, so it still quotes one there. report cannot tell an on-demand daemon that exits once idle from one started the same way that stays up, and the first report after upgrading can still call one gone, once. A filebrowser uninstalled by an earlier release does not get its database back by itself: the reinstall warns, and the steps to recover it are below.

⚠️ Behavior changes

  • docker_top and homebutler docker top --json return the kernel's name for each process in command, not its command line (#356). 1.3.0 returned the line exactly as docker top printed it, so a container started with --token=… or a password as a positional argument put it in the answer, including to an agent under --max-risk read. command now holds ps's comm: the executable's file name, at most 15 characters, which is the name processes reports. /usr/local/bin/probe serve --token=… becomes probe. It is not read from the command line, because a process sets its own argv[0] and can put an argument there. processes leaves kernel threads out, and docker_top still lists every row ps returns for the container. This is a deliberate exception to docs/compatibility.md. command is a frozen field, and its key and type stay, but what its value means narrows in this release without a deprecation period, because the old value was the disclosure. A script that read arguments out of it now gets only the name. The table homebutler docker top prints on your own terminal still shows the full line. On a host whose ps rejects -o pid,user,comm,args, the name falls back to the first word of the command line, with its directory removed. That can carry whatever a process put in its argv[0], but never more than that word. homebutler --server X docker top --json prints whatever the remote binary prints, so it keeps returning full command lines until that remote is upgraded. An MCP server on this release reduces a docker_top answer from an older remote to the first word of each line either way
  • A report taken just after something started no longer calls it new the next day (#315). The one-minute rule that keeps a shell pipeline out of report also kept young processes out of the saved baseline. A first report run half a minute after a container was recreated, from an ssh session that had just logged in, or from a tmux session just opened, left those processes out, and the next report named them new. A saved snapshot now keeps young processes, with a new optional key on each entry in processes: young is unconfirmed for one not yet seen running for a minute, and restarted for one that had been and was young again. A process counts once a snapshot has seen it a minute old. Until then it is neither new nor gone. That keeps the report's own ssh session, shell and | jq out of it when they are under a minute old at each report, and a job that runs for a few seconds every so often. A shell open for longer is counted, and is gone once it is closed. A later run of the same short job is a new instance rather than the one the baseline saw, so a run that hangs is new once it has been there a minute, as in 1.3.0. It also means that something never seen running for a minute that then stops is not reported. A new optional top-level key, processes_read_at, records when the process list was read. A snapshot from an earlier release has neither key, which reads as every process old, and that is what those releases kept. Going back to 1.3.0 afterwards can report the young processes a newer snapshot kept as gone, once
  • A daemon that restarted with the same command line within a minute of a report is no longer reported gone and then new (#315). It is the same process, as a restart with the same command line has always been everywhere else in report. A restart that changed the command line is still gone, then new once it has run for a minute
  • A daemon that keeps restarting is reported as state <name> restarting — under a minute old at this report and the last (#315). The line appears when a process had restarted just before the last report, after having been seen running for longer, and has started again since. 1.3.0 said gone once, on the first report after the loop began, and nothing after that. It says restarting rather than crashing, because the process table cannot tell the two apart. The realistic false alarm is a daemon reloaded on a schedule shortly before every report, as logrotate does, and the line is true of that too. A crash loop that changes its command line each time is a different process each time, and shows as gone once rather than restarting. One that was already looping at the first report has never been seen running for a minute, and gets no line
  • A daemon systemd starts on demand is no longer reported new or gone depending on whether it happened to be running (#363). fwupd and packagekit on Ubuntu are started over D-Bus when something asks them a question and exit once idle, and the soak and the demo host got gone: fwupd and new fwupd from nothing but timing. On Linux, report now reads each process's service from /proc/<pid>/cgroup and asks about all of them in one systemctl show call, which gets two seconds at most. A service whose unit file is static, that nothing wants or requires, and that a D-Bus name or a socket starts, is on demand, and its processes get no new or gone line. Nor do they get restarting when two reports in a row each catch one just after it was started. udisks2, which has a bus name but is enabled, is still reported, as is any enabled service that stops. A service a timer or a path unit starts is not on demand, so a hung apt-daily run is still new. A different command line under the same name is still replaced, on the report that first sees it. Whenever the service cannot be determined, the line is kept: macOS, no systemd, a process in a user session or a container, an unreadable cgroup, or a systemctl that fails or does not answer in time. The rule cannot tell a daemon that exits once idle from one started the same way that stays up, because systemd does not record which. polkitd is the one most hosts have: on a host where nothing requires polkit.service, it stopping is no longer reported. A saved process carries a new optional key, on_demand: true, written only where it is true, because a gone has to be judged from the older snapshot. A snapshot from an earlier release has no such key, so an on-demand daemon its baseline caught running is gone once on the first report after upgrading. The --json report shape is unchanged
  • filebrowser keeps its accounts and settings across uninstall and reinstall (#327). uninstall said the data was preserved and answered data_preserved: true, and the next install started with a new database and a new admin password. The image declares /config and /database as volumes, the compose file mapped only /srv, and each new container got new anonymous volumes. A new install now keeps them in ~/.homebutler/apps/filebrowser/config and ~/.homebutler/apps/filebrowser/database, beside data/ rather than in it, because data/ is /srv and anything in it can be browsed and downloaded. The container now runs as the user who installed it, where it used to run as uid 1000 whatever the host's uid was. On a host whose uid is not 1000 it could not write its state to a directory homebutler created, and it can now. If you chowned ~/.homebutler/apps/filebrowser/data to uid 1000 there so that uploads worked, chown it back to your own user, or filebrowser can no longer write to it. Two exceptions: under rootless Docker or Podman it runs as the container's root, which is the installing user on the host, and when homebutler runs as root (sudo) it runs as the image's own uid 1000 and the state directories are handed to that uid, rather than running filebrowser as root
    • An existing install still has its database in an anonymous volume. While its container exists, the new uninstall, or an install over a stopped container, stops it and copies /config and /database out into those directories before going on, and says so. A copy that fails stops it there, with the container stopped and not removed. A directory that already holds something is never overwritten. A container that was created and never ran, as a failed up leaves, does not count as the old one. A container still running is left alone until it is uninstalled. Upgrading homebutler changes nothing by itself. The volumes copied out of are kept by uninstall and removed by purge, which lists them with the rest

    • An install already uninstalled by an earlier release has no container left that names its volumes. The reinstall warns that it starts without them, and uninstall warns that they are not in the app directory. install_app, install_uninstall and their HTTP routes answer with the same text in a new optional warning field, present only then, and the dashboard's Apps card shows it. data_preserved keeps its meaning. The volumes are still there unless they were pruned. To bring the database back, stop the new install, find the unused volumes that hold a filebrowser.db, and copy the newest one in. Older ones can be from earlier reinstalls:

      homebutler install uninstall filebrowser
      for v in $(docker volume ls -q --filter dangling=true); do
        docker run --rm -v "$v":/v:ro busybox test -f /v/filebrowser.db && docker volume inspect -f '{{.CreatedAt}} {{.Name}}' "$v"
      done
      docker create --name filebrowser-recover -v <volume>:/database busybox
      rm ~/.homebutler/apps/filebrowser/database/filebrowser.db
      docker cp filebrowser-recover:/database/. ~/.homebutler/apps/filebrowser/database/
      docker rm filebrowser-recover
      homebutler install filebrowser
      
    • filebrowser's upstream project is archived and gets no further releases or security fixes. It stays in the catalogue, and install list and the README say so

  • tools/list carries MCP annotations, and seven write tools are marked destructive (#349, #357). A client that reads the hints can now ask before calling report, watch_check, watch_remove, backup_create, backup_drill, install_app and install_uninstall, although their class is still write. A single call to any of them can delete or overwrite something. report prunes snapshots down to the keep it is given. watch_check prunes incidents past the retention cap. watch_remove deletes the entry. backup_create prunes archives under backup.retention. backup_drill prunes drill records past the newest 50. install_app rewrites an existing docker-compose.yml. install_uninstall removes the containers, and whatever an app wrote inside them rather than to its directory. No class changed and --max-risk offers the same tools as before
  • Stopping watch start waits for its monitors (#360, #361). On SIGTERM or SIGINT it used to print Stopping all monitors. and exit at once. Now it exits once the monitors have stopped, which is normally immediate. A monitor that has not stopped after 10 seconds is left behind with a warning on stderr, and a second signal exits straight away. Under systemd this is well inside the default TimeoutStopSec

✨ Features

  • every MCP tool now carries readOnlyHint and destructiveHint in tools/list (#349, #357), so a client can ask before a destructive call in its own approval prompt. A read tool is read-only and not destructive. A destructive tool is destructive. A write tool is destructive when one call can delete or overwrite something. Under --max-risk read, report is listed as a read, with a description that says it never saves a snapshot. The hints complement --max-risk rather than replace it: a client may ignore them, and the server still refuses anything above the ceiling. docs/mcp-server.md says what destructiveHint: false does and does not mean

🐛 Fixes

  • a ps whose elapsed times could not be read made every process look as if it had just started (#315). 1.3.0 then saved no processes at all, and every report said skipped: processes — not compared — the previous snapshot has none, which was not the reason. An unreadable time is now unknown, and a process of unknown age is compared as an old one, as a process the invocation lookup missed already was
  • config validate quoted a Discord, Slack or generic webhook URL in full when it had no scheme, webhook token included (#358). The URL is the credential, and pasting one without https:// is exactly when the warning fired. config_validate is a read tool, so the token also reached an agent under --max-risk read. The warning now quotes a scheme written at the very start, if any, and the host, with … for the rest: discord.com/…, htps://discord.com/…. A user:password@ becomes …@, and anything not shaped like a host is quoted as …. A token written as the host itself still shows, since nothing tells it apart from a hostname
  • watch start warned continuing without thresholds or rules when only the alert rules failed to load (#342). The CPU, memory, disk and container checks read config.yaml's alerts block, which had already loaded, and kept running. The warning now says the rules are skipped and those checks still run, in the words config validate uses for the same failure, and adds that the notification channels are skipped too when the failure is in a standalone alerts file
  • every command, including report, status and doctor, waited five seconds on a terminal that does not answer a colour query (#344). The TUI library asks the terminal for its background colour as the binary starts and waits up to five seconds for an answer. On the Raspberry Pi with 1.2.0, report --no-save under a pty nobody answered took 6.10 s instead of 1.07 s. No homebutler style depends on the answer, so the question is no longer asked
  • stopping watch start left its docker events child running, reparented to pid 1, and every restart under nohup, tmux or a shell & added one more (#360). The process exited before the child was killed. On the Raspberry Pi, three restarts left three; now none are left. systemd setups were not affected, because the child died with the service's cgroup. Stopping while an incident was being gathered also killed the child without waiting for it, and that now goes through the same cleanup as an idle stop
  • install_purge over MCP and POST /api/install/{app}/purge can say that a volume was kept (#329). Since #326 the CLI lists a volume that still exists after a purge (external, or used by another container) as kept, and one Docker could not be asked about as unchecked, under Removed, except:. Both other surfaces answered {status: purged, app, data_preserved: false} either way, and the dashboard's Apps card printed Removed, with its data. after every purge, so an agent or an operator was told nothing was left while a volume still existed. The answer now carries kept_volumes and unchecked_volumes, each a list of volume names, present only when non-empty: a purge that removed everything answers exactly as before. The Apps card lists them under Removed, except:, as the CLI does. These are new optional fields on InstallResult, which 1.x allows. data_preserved is unchanged: it names the operation, true for uninstall and false for purge, not the outcome, and the two new fields are what say what is actually left

📚 Documentation

  • docker_top was described as read-only while it returned every command line in a container in full, and config_validate was not mentioned at all, though it names the config file, servers, endpoints and backup directory (#355). The read class says what a call does to the machine, and several places used it as if it also said what the call discloses. The README, docs/mcp-server.md, docs/compatibility.md and the agent skill now say what a read tool returns, not only that it changes nothing. docs/mcp-server.md also said docker_inspect returns configuration that can hold a credential: it never reads environment values, and what it does return is host mount paths and container addresses