# Bento deploy units `bentod` runs as a systemd **user** service on fw13 (the x86_64 build gate). Unlike `sandod` (a hardened system service with a dedicated `sando` user), bentod is a user service under the operator: building the apps needs the operator's SSH keys (to the tailnet build hosts and the mbp ops-agent), the app checkouts under `~/Code/Apps`, and the `_private` layer for signing secrets (`secrets_root`). A dedicated system user can't reach those without copying keys and bind-mounting home — so a user service is the right model, and it lets the operator redeploy bentod with `systemctl --user restart` (no sudo). ## Files | File | Where it goes | Purpose | |------|---------------|---------| | `bentod.service` | `~/.config/systemd/user/` | The user service unit. | | `bento-daemon.toml.example` | `~/.config/bento/bento-daemon.toml` | Daemon-local config (paths + listen). Absolute paths only — no shell expansion. | | `bento.toml.example` | `~/.config/bento/bento.toml` | Build topology (hosts + apps). `repo` paths are tilde-expanded. | | (none, hand-written 0600) | `~/.config/bento/bento.env` | `BENTO_API_TOKEN=...` for a tailnet bind. Read by the unit's `EnvironmentFile=-`, so a loopback install can leave it absent. No example file ships: it holds a secret. | ## Stand-up (no root except enable-linger) ```sh # 1. Build + install the binary cd ~/Code/MNW/bento/daemon && cargo build --release install -D -m 0755 target/release/bentod ~/.local/bin/bentod # 2. Config + state dirs mkdir -p ~/.config/bento ~/.local/state/bento/logs ~/Dist install -m 0644 ~/Code/MNW/bento/deploy/bento-daemon.toml.example ~/.config/bento/bento-daemon.toml install -m 0644 ~/Code/MNW/bento/deploy/bento.toml.example ~/.config/bento/bento.toml # (edit the two configs: absolute home paths, real host/app rows) # 3. Service mkdir -p ~/.config/systemd/user install -m 0644 ~/Code/MNW/bento/deploy/bentod.service ~/.config/systemd/user/ loginctl enable-linger "$USER" systemctl --user daemon-reload systemctl --user enable --now bentod # 4. Verify (address must match `listen` in bento-daemon.toml, see Auth below) curl -s http://127.0.0.1:8765/state | python3 -m json.tool ``` ## Auth (CF2) On a loopback bind, build triggers are reachable only from the daemon's own host, so no token is required. To operate bentod over the tailnet, bind the tailnet IP in `bento-daemon.toml` and set `BENTO_API_TOKEN` via an `EnvironmentFile` in the unit. bentod refuses to start on a non-loopback bind without it (same posture as Sando's `SANDO_API_TOKEN`). **fw13 runs the second form**, not the loopback default the example config ships. `listen` is the tailnet address and `/build` and `/retry` require the bearer token, so a `127.0.0.1` curl gets no answer at all rather than an auth error. Read the address out of the config and the token out of its own file rather than passing either on a command line: ```sh set -a; . ~/.config/bento/bento.env; set +a curl -s -X POST http://$(grep -oP '(?<=^listen = ")[^"]+' ~/.config/bento/bento-daemon.toml)/build \ -H "authorization: Bearer $BENTO_API_TOKEN" -H 'content-type: application/json' \ -d '{"app":"makeover-webview","version":"0.5.1"}' ``` `/state`, `/status.json` and the log endpoints are read-only and unauthed, so watching a run needs the address but not the token. ## Recipes A real `/build` reads `//.rhai` from each app's checkout. The recipes now exist, one per platform per app: | App | macOS | iOS | Linux | Windows | |-----|-------|-----|-------|---------| | goingson | yes | yes | yes | yes (unsigned, unsupported) | | audiofiles | yes | — (egui, no iOS) | yes | yes (unsigned, unsupported) | | balanced_breakfast | pending¹ | pending¹ | yes | yes (unsigned, unsupported) | ¹ BB has no Developer ID signing / notarization infra or iOS project yet, so its macOS/iOS recipes are deferred until that lands. BB ships in a later wave. A library crate (`kind = "library"` in its own `bento.toml`) has no per-platform artifact, so the runner reads a single `publish.rhai` instead of the table above. Every library configured in the topology now carries one: `makeover`, `makeover-geometry`, `makeover-build`, `makeover-webview`, `makeover-layout`, `pter`, `everycycle`, `supernote-push`, `alloy_tui`. That recipe asserts HEAD is exactly a tag before it uploads, so a release wants `git tag -a v` first; a crate published by hand before it was wired up has no such tag, and its next release is the one that gets one. A `linux.rhai` serves both arches — `build_host()` resolves to the native host the topology assigns the target (`linux/x86_64` → fw13, `linux/aarch64` → astra), so there is no cross-compilation and no hard-coded host name. macOS/iOS recipes run on the mbp `agent`-transport host where the Developer ID key is usable. ### The prebuild gate Every recipe runs a `prebuild` step between checkout and build: ``` cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` `sh_ok` aborts the recipe on a non-zero exit, and a failed step lands in the step ledger, so a red gate bars publish as well as build. It runs on the target's own build host rather than once centrally: a break confined to one platform (cfg-gated code, a Windows path) is then caught on the machine that would have shipped it. The cost is that the suite runs once per target. Features come from `feature_flags()`, so the gate compiles the same configuration the release build does. ### Recipe host-function vocabulary Beyond `step`/`sh`/`sh_ok`/`log`/`collect`/`publish`/`secret`/`env` and the macOS helpers (`codesign`/`notarize`/`staple`/`verify_gatekeeper`/`keychain_*`), recipes can read their own context: | Function | Returns | |----------|---------| | `version()` | the version being built (e.g. `"0.4.2"`) | | `build_host()` | the host this target builds on (e.g. `"fw13"`) | | `repo()` | the app's checkout path (`~`-prefixed; `cd` into it — commands don't auto-cd) | | `target()` / `platform()` / `arch()` | `"linux/x86_64"` / `"linux"` / `"x86_64"` | Non-Tauri apps (audiofiles) set `version_path` in the topology to the crate `Cargo.toml` carrying the version, since they have no `tauri.conf.json`.