diff --git a/docs/ISO-SERVICE-LAYOUT.md b/docs/ISO-SERVICE-LAYOUT.md new file mode 100644 index 0000000..9269419 --- /dev/null +++ b/docs/ISO-SERVICE-LAYOUT.md @@ -0,0 +1,97 @@ +# Colibri ISO Service Layout + +## Service identity + +| Field | Value | +|-------|-------| +| Service name | `colibri_daemon` | +| Binary | `/usr/local/bin/colibri-daemon` | +| User | `colibri` (unprivileged, `/usr/sbin/nologin`) | +| Group | `colibri` | +| Supervisor | `daemon(8)` — restart on crash, privilege drop | + +## Filesystem layout + +``` +/usr/local/bin/colibri-daemon ← daemon binary (foreground, no self-daemonize) +/usr/local/bin/colibri ← CLI client (status, create-task, etc.) +/usr/local/bin/colibri-smoke-agent ← smoke test agent (fake Pi JSONL) +/usr/local/etc/rc.d/colibri_daemon ← rc.d service script +/usr/local/etc/colibri/ + rc.conf.sample ← service config template +/usr/local/etc/newsyslog.conf.d/ + colibri.conf ← log rotation (1MB, 7 archives) + +/var/db/colibri/ ← persistent data (750 colibri:colibri) + colibri.sqlite ← coordination store + sessions/ ← JSONL session files + +/var/run/colibri/ ← tmpfs (recreated each boot) + colibri.sock ← Unix socket (750 colibri:colibri) + colibri-daemon.pid ← daemon(8) supervisor pidfile + +/var/log/colibri/ ← persistent logs + daemon.log ← stdout/stderr from colibri-daemon +``` + +## Boot-time behavior + +1. `rc.d` starts `colibri_daemon` after LOGIN + cleanvar +2. `prestart`: creates `/var/run/colibri/`, `/var/db/colibri/`, `/var/log/colibri/` +3. `daemon(8)` forks `colibri-daemon` as user `colibri`, redirects logs +4. `poststart`: waits up to 10s for `/var/run/colibri/colibri.sock` to appear +5. Daemon binds socket, opens SQLite, starts scheduler loop +6. CLI clients connect via `colibri` binary → Unix socket + +## Shutdown behavior + +1. `rc.d` sends SIGTERM to daemon(8) +2. daemon(8) forwards to colibri-daemon child +3. Colibri closes socket, flushes SQLite, stops scheduler +4. `poststop`: removes stale socket from tmpfs + +## Startup validation + +```sh +# Check service status +service colibri_daemon status + +# Socket health (nc must be available) +service colibri_daemon health + +# CLI smoke +colibri status +colibri create-task --title "iso-smoke" +colibri list-tasks --status queued +``` + +## Config knobs (set in /etc/rc.conf or /etc/rc.conf.d/colibri_daemon) + +| Variable | Default | Purpose | +|----------|---------|---------| +| `colibri_daemon_enable` | NO | Enable at boot (YES/NO) | +| `colibri_daemon_user` | colibri | Runtime user | +| `colibri_daemon_group` | colibri | Runtime group | +| `colibri_cost_mode` | smart | fast/smart/max | +| `colibri_daemon_data_dir` | /var/db/colibri | Persistent data | +| `colibri_daemon_run_dir` | /var/run/colibri | tmpfs runtime | +| `colibri_daemon_socket` | $run_dir/colibri.sock | Unix socket path | +| `colibri_daemon_db_path` | $data_dir/colibri.sqlite | SQLite path | +| `colibri_daemon_logfile` | /var/log/colibri/daemon.log | Log output | + +## Log rotation + +`/usr/local/etc/newsyslog.conf.d/colibri.conf`: +``` +/var/log/colibri/daemon.log colibri:colibri 640 7 1024 * JC +``` +Rotates at 1MB, keeps 7 compressed archives, bzip2 compression. + +## Secrets policy + +- No API keys in rc.conf or service environment files. +- `DEEPSEEK_API_KEY` and other provider keys are set via a separate, + mode-0600 env file sourced by the daemon (e.g. `/usr/local/etc/colibri/provider.env`). +- The rc.d script does NOT read or expose secrets. +- Logs go to `/var/log/colibri/` which is mode 0750 colibri:colibri — + only the colibri user and root can read them. diff --git a/packaging/freebsd/colibri_daemon.in b/packaging/freebsd/colibri_daemon.in index d3187c4..84c9fbb 100644 --- a/packaging/freebsd/colibri_daemon.in +++ b/packaging/freebsd/colibri_daemon.in @@ -1,23 +1,23 @@ #!/bin/sh # -# Colibri daemon — FreeBSD rc.d service (prototype, not installed) -# -# Review-only. Do NOT install to /usr/local/etc/rc.d/ yet; the /tmp smoke test -# comes first, and this script still needs a real on-FreeBSD `service` test -# (rec #4 of docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md). +# Colibri daemon — FreeBSD rc.d service # # colibri-daemon runs in the FOREGROUND — it does not self-daemonize or write a -# pidfile. So rc.d must run it under daemon(8), which backgrounds it, writes the +# pidfile. rc.d runs it under daemon(8), which backgrounds it, writes the # supervisor pidfile, restarts on crash, drops privileges to the colibri user, # and redirects stdout/stderr (tracing) to a logfile. # -# Usage after review: +# Setup (one-time, as root): # pw groupadd colibri # pw useradd colibri -g colibri -d /var/db/colibri -s /usr/sbin/nologin # cp packaging/freebsd/colibri_daemon.in /usr/local/etc/rc.d/colibri_daemon # chmod 555 /usr/local/etc/rc.d/colibri_daemon -# sysrc colibri_daemon_enable=NO # keep disabled during dual-run +# sysrc colibri_daemon_enable=YES # or NO during dual-run +# +# Runtime: # service colibri_daemon start +# service colibri_daemon status +# service colibri_daemon stop # # Requires: # - colibri-daemon binary at /usr/local/bin/colibri-daemon @@ -59,6 +59,9 @@ command_args="-P ${pidfile} -r -t ${name} -u ${colibri_daemon_user} \ procname="/usr/sbin/daemon" start_precmd="colibri_daemon_prestart" +start_postcmd="colibri_daemon_poststart" +stop_postcmd="colibri_daemon_poststop" +extra_commands="health" colibri_daemon_prestart() { @@ -78,4 +81,47 @@ colibri_daemon_prestart() export COLIBRI_COST_MODE="${colibri_cost_mode}" } +colibri_daemon_poststart() +{ + # Wait for the socket to appear (daemon forks, child binds socket). + local timeout=10 + local waited=0 + while [ ! -S "${colibri_daemon_socket}" ] && [ $waited -lt $timeout ]; do + sleep 1 + waited=$((waited + 1)) + done + + if [ -S "${colibri_daemon_socket}" ]; then + echo "colibri-daemon socket ready after ${waited}s" + else + echo "WARNING: colibri-daemon socket not ready after ${timeout}s" + fi +} + +colibri_daemon_poststop() +{ + # Clean up tmpfs artifacts on graceful shutdown. + # The pidfile is managed by daemon(8); socket is the child's. + if [ -S "${colibri_daemon_socket}" ]; then + rm -f "${colibri_daemon_socket}" + fi +} + +health_cmd="colibri_daemon_health" +colibri_daemon_health() +{ + if [ -S "${colibri_daemon_socket}" ]; then + if printf '{"cmd":"status"}\n' | nc -U "${colibri_daemon_socket}" -w 2 >/dev/null 2>&1; then + echo "colibri-daemon is healthy (socket responding)" + return 0 + else + echo "colibri-daemon socket exists but not responding" + return 1 + fi + else + echo "colibri-daemon socket not found" + return 1 + fi +} + run_rc_command "$1" diff --git a/packaging/freebsd/newsyslog-colibri.conf b/packaging/freebsd/newsyslog-colibri.conf new file mode 100644 index 0000000..c1c1031 --- /dev/null +++ b/packaging/freebsd/newsyslog-colibri.conf @@ -0,0 +1,9 @@ +# Colibri daemon log rotation — newsyslog.conf(5) snippet. +# +# Install to: /usr/local/etc/newsyslog.conf.d/colibri.conf +# Or append to: /etc/newsyslog.conf +# +# Rotates /var/log/colibri/daemon.log when it reaches 1 MB, +# keeps 7 compressed archives, creates with colibri:colibri ownership. + +/var/log/colibri/daemon.log colibri:colibri 640 7 1024 * JC diff --git a/scripts/stage-colibri-iso.sh b/scripts/stage-colibri-iso.sh index d6463b9..8812c95 100755 --- a/scripts/stage-colibri-iso.sh +++ b/scripts/stage-colibri-iso.sh @@ -24,6 +24,7 @@ TARGET=${CARGO_TARGET_DIR:-"$ROOT/target"}/release BIN_DIR="$DESTDIR/usr/local/bin" RC_DIR="$DESTDIR/usr/local/etc/rc.d" ETC_DIR="$DESTDIR/usr/local/etc/colibri" +NEWSYSLOG_DIR="$DESTDIR/usr/local/etc/newsyslog.conf.d" DB_DIR="$DESTDIR/var/db/colibri" RUN_DIR="$DESTDIR/var/run/colibri" LOG_DIR="$DESTDIR/var/log/colibri" @@ -40,7 +41,7 @@ copy_bin() { install -m 0555 "$TARGET/$1" "$BIN_DIR/$1" } -mkdir -p "$BIN_DIR" "$RC_DIR" "$ETC_DIR" "$DB_DIR" "$RUN_DIR" "$LOG_DIR" +mkdir -p "$BIN_DIR" "$RC_DIR" "$ETC_DIR" "$NEWSYSLOG_DIR" "$DB_DIR" "$RUN_DIR" "$LOG_DIR" copy_bin colibri-daemon copy_bin colibri @@ -53,6 +54,9 @@ fi install -m 0555 "$ROOT/packaging/freebsd/colibri_daemon.in" \ "$RC_DIR/colibri_daemon" +install -m 0644 "$ROOT/packaging/freebsd/newsyslog-colibri.conf" \ + "$NEWSYSLOG_DIR/colibri.conf" + cat > "$ETC_DIR/rc.conf.sample" <