| 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 |
|
| 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 |
| (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. | |
| 20 |
|
| 21 |
## Stand-up (no root except enable-linger) |
| 22 |
|
| 23 |
```sh |
| 24 |
# 1. Build + install the binary |
| 25 |
cd ~/Code/MNW/bento/daemon && cargo build --release |
| 26 |
install -D -m 0755 target/release/bentod ~/.local/bin/bentod |
| 27 |
|
| 28 |
# 2. Config + state dirs |
| 29 |
mkdir -p ~/.config/bento ~/.local/state/bento/logs ~/Dist |
| 30 |
install -m 0644 ~/Code/MNW/bento/deploy/bento-daemon.toml.example ~/.config/bento/bento-daemon.toml |
| 31 |
install -m 0644 ~/Code/MNW/bento/deploy/bento.toml.example ~/.config/bento/bento.toml |
| 32 |
# (edit the two configs: absolute home paths, real host/app rows) |
| 33 |
|
| 34 |
# 3. Service |
| 35 |
mkdir -p ~/.config/systemd/user |
| 36 |
install -m 0644 ~/Code/MNW/bento/deploy/bentod.service ~/.config/systemd/user/ |
| 37 |
loginctl enable-linger "$USER" |
| 38 |
systemctl --user daemon-reload |
| 39 |
systemctl --user enable --now bentod |
| 40 |
|
| 41 |
# 4. Verify (address must match `listen` in bento-daemon.toml, see Auth below) |
| 42 |
curl -s http://127.0.0.1:8765/state | python3 -m json.tool |
| 43 |
``` |
| 44 |
|
| 45 |
## Auth (CF2) |
| 46 |
|
| 47 |
On a loopback bind, build triggers are reachable only from the daemon's own |
| 48 |
host, so no token is required. To operate bentod over the tailnet, bind the |
| 49 |
tailnet IP in `bento-daemon.toml` and set `BENTO_API_TOKEN` via an |
| 50 |
`EnvironmentFile` in the unit. bentod refuses to start on a non-loopback bind |
| 51 |
without it (same posture as Sando's `SANDO_API_TOKEN`). |
| 52 |
|
| 53 |
**fw13 runs the second form**, not the loopback default the example config |
| 54 |
ships. `listen` is the tailnet address and `/build` and `/retry` require the |
| 55 |
bearer token, so a `127.0.0.1` curl gets no answer at all rather than an auth |
| 56 |
error. Read the address out of the config and the token out of its own file |
| 57 |
rather than passing either on a command line: |
| 58 |
|
| 59 |
```sh |
| 60 |
set -a; . ~/.config/bento/bento.env; set +a |
| 61 |
curl -s -X POST http://$(grep -oP '(?<=^listen = ")[^"]+' ~/.config/bento/bento-daemon.toml)/build \ |
| 62 |
-H "authorization: Bearer $BENTO_API_TOKEN" -H 'content-type: application/json' \ |
| 63 |
-d '{"app":"makeover-webview","version":"0.5.1"}' |
| 64 |
``` |
| 65 |
|
| 66 |
`/state`, `/status.json` and the log endpoints are read-only and unauthed, so |
| 67 |
watching a run needs the address but not the token. |
| 68 |
|
| 69 |
## Recipes |
| 70 |
|
| 71 |
A real `/build` reads `<app>/<recipe_dir>/<platform>.rhai` from each app's |
| 72 |
checkout. The recipes now exist, one per platform per app: |
| 73 |
|
| 74 |
|
| 75 |
|
| 76 |
| goingson | yes | yes | yes | yes (unsigned, unsupported) | |
| 77 |
| audiofiles | yes | — (egui, no iOS) | yes | yes (unsigned, unsupported) | |
| 78 |
| balanced_breakfast | pending¹ | pending¹ | yes | yes (unsigned, unsupported) | |
| 79 |
|
| 80 |
¹ BB has no Developer ID signing / notarization infra or iOS project yet, so its |
| 81 |
macOS/iOS recipes are deferred until that lands. BB ships in a later wave. |
| 82 |
|
| 83 |
A library crate (`kind = "library"` in its own `bento.toml`) has no per-platform |
| 84 |
artifact, so the runner reads a single `publish.rhai` instead of the table above. |
| 85 |
Every library configured in the topology now carries one: `makeover`, |
| 86 |
`makeover-geometry`, `makeover-build`, `makeover-webview`, `makeover-layout`, |
| 87 |
`pter`, `everycycle`, `supernote-push`, `alloy_tui`. That recipe asserts HEAD is |
| 88 |
exactly a tag before it uploads, so a release wants `git tag -a v<version>` |
| 89 |
first; a crate published by hand before it was wired up has no such tag, and its |
| 90 |
next release is the one that gets one. |
| 91 |
|
| 92 |
A `linux.rhai` serves both arches — `build_host()` resolves to the native host |
| 93 |
the topology assigns the target (`linux/x86_64` → fw13, `linux/aarch64` → astra), |
| 94 |
so there is no cross-compilation and no hard-coded host name. macOS/iOS recipes |
| 95 |
run on the mbp `agent`-transport host where the Developer ID key is usable. |
| 96 |
|
| 97 |
### The prebuild gate |
| 98 |
|
| 99 |
Every recipe runs a `prebuild` step between checkout and build: |
| 100 |
|
| 101 |
``` |
| 102 |
cargo clippy --workspace --all-targets <features> -- -D warnings |
| 103 |
cargo test --workspace <features> |
| 104 |
``` |
| 105 |
|
| 106 |
`sh_ok` aborts the recipe on a non-zero exit, and a failed step lands in the |
| 107 |
step ledger, so a red gate bars publish as well as build. It runs on the target's |
| 108 |
own build host rather than once centrally: a break confined to one platform |
| 109 |
(cfg-gated code, a Windows path) is then caught on the machine that would have |
| 110 |
shipped it. The cost is that the suite runs once per target. |
| 111 |
|
| 112 |
Features come from `feature_flags()`, so the gate compiles the same |
| 113 |
configuration the release build does. |
| 114 |
|
| 115 |
### Recipe host-function vocabulary |
| 116 |
|
| 117 |
Beyond `step`/`sh`/`sh_ok`/`log`/`collect`/`publish`/`secret`/`env` and the |
| 118 |
macOS helpers (`codesign`/`notarize`/`staple`/`verify_gatekeeper`/`keychain_*`), |
| 119 |
recipes can read their own context: |
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
| `version()` | the version being built (e.g. `"0.4.2"`) | |
| 124 |
| `build_host()` | the host this target builds on (e.g. `"fw13"`) | |
| 125 |
| `repo()` | the app's checkout path (`~`-prefixed; `cd` into it — commands don't auto-cd) | |
| 126 |
| `target()` / `platform()` / `arch()` | `"linux/x86_64"` / `"linux"` / `"x86_64"` | |
| 127 |
|
| 128 |
Non-Tauri apps (audiofiles) set `version_path` in the topology to the crate |
| 129 |
`Cargo.toml` carrying the version, since they have no `tauri.conf.json`. |
| 130 |
|