Skip to main content

max / makenotwork

bento: stand up bentod as a systemd user service bentod runs as a user service (not a hardened system service like sandod): building the apps needs the operator's SSH keys, the ~/Code/Apps checkouts, and the _private signing layer (secrets_root), which a dedicated system user can't reach. A user service also lets the operator redeploy with `systemctl --user restart` — no sudo. deploy/: bentod.service (user unit, %h paths, loopback bind), plus bento-daemon.toml.example and bento.toml.example (4 hosts: fw13/astra/mbp/ windows-x86; 3 apps: goingson/balanced_breakfast/audiofiles), and a README with the no-root stand-up. Loopback bind needs no token; tailnet bind would require BENTO_API_TOKEN (CF2), documented. Stood up live on fw13: hosts=4 apps=3, listening 127.0.0.1:8765, linger enabled. Builds still need per-app recipes (none exist yet) — that's the next piece. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-12 22:19 UTC
Commit: 9c59e6d17db9838ef368cacb3555900c95ec7d67
Parent: 89486d0
4 files changed, +172 insertions, -0 deletions
@@ -0,0 +1,57 @@
1 + # Bento deploy units
2 +
3 + `bentod` runs as a systemd **user** service on fw13 (the x86_64 build gate).
4 + Unlike `sandod` (a hardened system service with a dedicated `sando` user),
5 + bentod is a user service under the operator: building the apps needs the
6 + operator's SSH keys (to the tailnet build hosts and the mbp ops-agent), the app
7 + checkouts under `~/Code/Apps`, and the `_private` layer for signing secrets
8 + (`secrets_root`). A dedicated system user can't reach those without copying keys
9 + and bind-mounting home — so a user service is the right model, and it lets the
10 + operator redeploy bentod with `systemctl --user restart` (no sudo).
11 +
12 + ## Files
13 +
14 + | File | Where it goes | Purpose |
15 + |------|---------------|---------|
16 + | `bentod.service` | `~/.config/systemd/user/` | The user service unit. |
17 + | `bento-daemon.toml.example` | `~/.config/bento/bento-daemon.toml` | Daemon-local config (paths + listen). Absolute paths only — no shell expansion. |
18 + | `bento.toml.example` | `~/.config/bento/bento.toml` | Build topology (hosts + apps). `repo` paths are tilde-expanded. |
19 +
20 + ## Stand-up (no root except enable-linger)
21 +
22 + ```sh
23 + # 1. Build + install the binary
24 + cd ~/Code/MNW/bento/daemon && cargo build --release
25 + install -D -m 0755 target/release/bentod ~/.local/bin/bentod
26 +
27 + # 2. Config + state dirs
28 + mkdir -p ~/.config/bento ~/.local/state/bento/logs ~/Dist
29 + install -m 0644 ~/Code/MNW/bento/deploy/bento-daemon.toml.example ~/.config/bento/bento-daemon.toml
30 + install -m 0644 ~/Code/MNW/bento/deploy/bento.toml.example ~/.config/bento/bento.toml
31 + # (edit the two configs: absolute home paths, real host/app rows)
32 +
33 + # 3. Service
34 + mkdir -p ~/.config/systemd/user
35 + install -m 0644 ~/Code/MNW/bento/deploy/bentod.service ~/.config/systemd/user/
36 + loginctl enable-linger "$USER"
37 + systemctl --user daemon-reload
38 + systemctl --user enable --now bentod
39 +
40 + # 4. Verify
41 + curl -s http://127.0.0.1:8765/state | python3 -m json.tool
42 + ```
43 +
44 + ## Auth (CF2)
45 +
46 + On the loopback bind above, build triggers are reachable only from fw13, so no
47 + token is required. To operate bentod over the tailnet, bind the tailnet IP in
48 + `bento-daemon.toml` and set `BENTO_API_TOKEN` via an `EnvironmentFile` in the
49 + unit — bentod refuses to start on a non-loopback bind without it (same posture
50 + as Sando's `SANDO_API_TOKEN`).
51 +
52 + ## Not yet: builds need recipes
53 +
54 + Standing up the service makes bentod reachable (TUI/driver, `/state`), but a
55 + real `/build` reads `<app>/<recipe_dir>/<platform>.rhai` from each app's
56 + checkout. Those recipes do not exist yet — writing them (per-app, per-platform)
57 + is the next piece before bentod can actually ship an app.
@@ -0,0 +1,26 @@
1 + # Bento daemon-local config (machine paths + listen). Install at
2 + # ~/.config/bento/bento-daemon.toml. The build matrix lives in the separate
3 + # topology file (bento.toml).
4 + #
5 + # These paths are NOT tilde-expanded by the daemon (it runs without a shell) —
6 + # use absolute paths. Replace /home/max with the daemon user's home.
7 + #
8 + # Auth (CF2): with a non-loopback `listen`, bentod REFUSES to start unless
9 + # BENTO_API_TOKEN is set (via the service's environment). On the loopback bind
10 + # below, build triggers are reachable only from this host, so no token is
11 + # required. To operate over the tailnet, bind the tailnet IP and set
12 + # BENTO_API_TOKEN (mirror Sando's sando.env model).
13 +
14 + listen = "127.0.0.1:8765"
15 + db_path = "/home/max/.local/state/bento/bento.db"
16 + topology_path = "/home/max/.config/bento/bento.toml"
17 +
18 + # Root of the Syncthing private layer; the recipe `secret()` host function reads
19 + # credential files (signing keys, notary creds) relative to here. Never logged.
20 + secrets_root = "/home/max/Code/_private"
21 +
22 + # Collected artifacts land at <dist_root>/<app>/<version>/.
23 + dist_root = "/home/max/Dist"
24 +
25 + # Per-step run logs: <logs_root>/<app>/<version>/<target>/<step>.log
26 + logs_root = "/home/max/.local/state/bento/logs"
@@ -0,0 +1,52 @@
1 + # Bento build topology: the native build hosts and the apps they ship.
2 + # Install at ~/.config/bento/bento.toml (path set by `topology_path` in
3 + # bento-daemon.toml). Paths under `repo` are tilde-expanded by the engine; the
4 + # daemon-local paths in bento-daemon.toml are NOT — use absolute paths there.
5 + #
6 + # House rule: no cross-compilation. A target builds only on a host whose row
7 + # lists it (topology::host_for); validate() rejects an app target no host
8 + # declares, and a host with targets that doesn't grant `build`.
9 +
10 + [[host]]
11 + name = "fw13"
12 + ssh = "fw13" # tailnet alias; the daemon's own host (local)
13 + targets = ["linux/x86_64"]
14 +
15 + [[host]]
16 + name = "astra"
17 + ssh = "astra"
18 + targets = ["linux/aarch64"]
19 +
20 + [[host]]
21 + name = "mbp"
22 + ssh = "mbp"
23 + targets = ["macos/aarch64", "ios/universal"]
24 + transport = "agent" # in-session ops-agent — required to codesign
25 + agent_url = "http://mbp:8765" # tailnet addr of the Mac's ops-agent
26 + actuate = ["build", "sign", "notarize", "staple"]
27 + observe = ["build-log", "gatekeeper"]
28 +
29 + [[host]]
30 + name = "windows-x86"
31 + ssh = "me@windows-x86"
32 + targets = ["windows/x86_64"]
33 + # transport defaults to "ssh"; actuate defaults to ["build", "package"]
34 +
35 + [app.goingson]
36 + repo = "~/Code/Apps/goingson"
37 + branch = "main"
38 + recipe_dir = "dist/recipes"
39 + targets = ["macos/aarch64", "ios/universal", "linux/x86_64", "linux/aarch64", "windows/x86_64"]
40 +
41 + [app.balanced_breakfast]
42 + repo = "~/Code/Apps/balanced_breakfast"
43 + branch = "main"
44 + recipe_dir = "dist/recipes"
45 + targets = ["macos/aarch64", "ios/universal", "linux/x86_64", "linux/aarch64", "windows/x86_64"]
46 +
47 + [app.audiofiles]
48 + repo = "~/Code/Apps/audiofiles"
49 + branch = "main"
50 + recipe_dir = "dist/recipes"
51 + # egui desktop app (no Tauri, no iOS).
52 + targets = ["macos/aarch64", "linux/x86_64", "linux/aarch64", "windows/x86_64"]
@@ -0,0 +1,37 @@
1 + # Bento app-build controller (bentod) — systemd USER service under the operator.
2 + #
3 + # bentod is a user service (not system) because it builds the apps and so needs
4 + # the operator's environment directly: SSH keys to the tailnet build hosts + the
5 + # mbp ops-agent, the app checkouts under ~/Code/Apps, and the _private layer for
6 + # signing secrets (secrets_root). A hardened system user can't reach those.
7 + #
8 + # Install (one-time, no sudo except enable-linger):
9 + # mkdir -p ~/.config/systemd/user
10 + # install -m 0644 bentod.service ~/.config/systemd/user/
11 + # loginctl enable-linger "$USER" # keep it running across logout/reboot
12 + # systemctl --user daemon-reload
13 + # systemctl --user enable --now bentod
14 + #
15 + # Watch: journalctl --user -u bentod -f
16 + # Deploy a new bentod: build, copy to ~/.local/bin/bentod, `systemctl --user
17 + # restart bentod` (no sudo — that's the point of a user service).
18 + [Unit]
19 + Description=Bento app build controller
20 + After=network-online.target
21 + Wants=network-online.target
22 +
23 + [Service]
24 + Type=simple
25 + ExecStart=%h/.local/bin/bentod
26 + Restart=on-failure
27 + RestartSec=5
28 + Environment=BENTO_CONFIG=%h/.config/bento/bento-daemon.toml
29 + # Loopback bind (default) needs no token. For a tailnet bind, set listen to the
30 + # tailnet IP in bento-daemon.toml AND provide BENTO_API_TOKEN here, e.g.:
31 + # EnvironmentFile=-%h/.config/bento/bento.env # contains BENTO_API_TOKEN=...
32 + StandardOutput=journal
33 + StandardError=journal
34 + SyslogIdentifier=bentod
35 +
36 + [Install]
37 + WantedBy=default.target