From ac5cb6f6f26877ab2e551db8c03836ee833f88ce Mon Sep 17 00:00:00 2001 From: Nicolas CHAUVIN Date: Thu, 10 Sep 2026 08:56:52 +0200 Subject: [PATCH] docs(readme): stop teaching the observe that cost #486 --- CHANGELOG.md | 2 ++ README.md | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d8da2..e92423e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- The README no longer teaches the observe that cost #486: its `def` example asked `dpkg -s`, which exits 0 for a package removed without `--purge`. It now carries `apt.install`'s real question and says why. `--json`, `-v`, `--parallel` and `--limit` are documented, and the secrets note states the limit that is left rather than one ADR-0025 closed (#642). + - `htpasswd.entry` and `system.timezone` observe everything their apply sets. The first left a credentials file world-readable whenever its hash already verified; the second left `/etc/timezone` naming another zone whenever the symlink was right. Both reported `already`. Two adverse cases, each seen red on a real machine first (#635, #636). - A def calling another instruction with too few arguments is refused instead of binding the missing one to the empty string. `file.write(path)` inside a def overwrote the file with nothing and reported `ok.done` — a file destroyed under a success verdict. A plan-level call was always checked on both bounds; only the def side was not (#633). diff --git a/README.md b/README.md index b4b73d5..8d8a032 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,12 @@ shellf run plan.shellf --secret-file rclone_pass=./secret --secret-env db=DB_PW ``` A secret is a variable like any other (`${rclone_pass}`), but shellf **redacts -its value** (`***`) from every report, `--dry-run`, and `status`. Honest limit: the -secret still reaches the target (in the request file, `0600`, and the process -env) — root there can read it; at-rest secrecy is not yet solved. +its value** (`***`) from every report, `--dry-run`, and `status`. At rest it is kept off +persistent disk: the agent's job files live on tmpfs (`/dev/shm`), so a secret is outside +backups, snapshots and undelete — falling back to `/tmp`, owner-only and deleted after, on a +host with no writable `/dev/shm` (ADR-0025). Honest limit: the secret still reaches the target, +in that job file and in the process environment, and **root there reads both in real time**. +No on-target scheme changes that, encryption included — the key would have to travel too. **Control flow.** `if` takes an instruction (or a captured result); the branch is taken on its outcome. `dir.exists` is a read-only *question*, so it stays honest @@ -211,7 +214,10 @@ its success: ``` def install(pkg: str) as root { observe { - return state(installed: shell { dpkg -s "$pkg" >/dev/null 2>&1 }.exit == 0) + # `install ok installed` is the one status meaning the files are on the host + return state(installed: shell { + dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q '^install ok installed' + }.exit == 0) } apply { r = shell { apt-get install -y "$pkg" } @@ -221,6 +227,12 @@ def install(pkg: str) as root { } ``` +That is `apt.install`'s real observe, and the comment is the whole lesson: **the question an +`observe` asks has to be the one its `apply` answers**. This def first asked `dpkg -s`, which +exits 0 for a package removed without `--purge` — its config files survive, its binaries do +not — so it reported `already` on a host where the package was gone. The wrong answer was +*stable*, so re-running agreed with itself and only the machine said otherwise (#486). + A field with no same-named argument (like `installed`) must simply hold; fields that match a parameter (`service.ensure` → `running`, `git.clone` → `url`) are compared to it. See [ADR-0013](docs/adr/0013-observe-state-contract.md). @@ -352,6 +364,13 @@ agents and wipes shellf's files from the targets. | `--known-hosts ` | host-key file (default `~/.ssh/known_hosts`) | | `--insecure` | skip host-key verification (dev only) | | `--agent-ttl ` | resident agent inactivity TTL before it self-erases (default 2h) | +| `--parallel ` | hosts dialled at once (default 16); `1` serialises the fan-out | +| `--limit ` | restrict the run to a host or group (repeatable); narrows the plan, never extends it | +| `--json` | report as JSON on stdout; diagnostics stay on stderr | +| `-v` | trace the control host's decisions, and every command run on the target | + +`run` takes all of them. `status` takes all but `--vars`, `--set`, `--dry-run`, `-v` and +`--agent-ttl`. `clean` takes `--inventory`, `--insecure` and `--known-hosts`. ## How it works