feat: ISO service hardening — rc.d + log rotation + layout docs #7
4 changed files with 166 additions and 9 deletions
97
docs/ISO-SERVICE-LAYOUT.md
Normal file
97
docs/ISO-SERVICE-LAYOUT.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
9
packaging/freebsd/newsyslog-colibri.conf
Normal file
9
packaging/freebsd/newsyslog-colibri.conf
Normal file
|
|
@ -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
|
||||
|
|
@ -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" <<EOF
|
||||
# Colibri control plane service defaults for the Clawdie ISO.
|
||||
# Copy or merge into /etc/rc.conf or /etc/rc.conf.d/colibri_daemon.
|
||||
|
|
@ -101,6 +105,7 @@ Installed:
|
|||
/usr/local/bin/colibri-smoke-agent
|
||||
/usr/local/etc/rc.d/colibri_daemon
|
||||
/usr/local/etc/colibri/rc.conf.sample
|
||||
/usr/local/etc/newsyslog.conf.d/colibri.conf
|
||||
|
||||
Next image integration steps:
|
||||
1. Ensure colibri user/group exists in the image.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue