diff --git a/build.sh b/build.sh index 55800bfb..1b017640 100755 --- a/build.sh +++ b/build.sh @@ -497,6 +497,15 @@ check_release_gate() { fi fi + # Guard against a re-introduced duplicate port. The canonical Colibri FreeBSD + # port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri) and + # is consumed from there; a copy in this repo would silently drift from + # Colibri's Cargo.lock / binaries / license. + if [ -e "${SCRIPT_DIR}/ports/sysutils/colibri" ]; then + echo "ERROR: duplicate Colibri port at ports/sysutils/colibri — the canonical port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri); remove this copy." + _release_errors=$(( _release_errors + 1 )) + fi + if [ "${_release_errors}" -gt 0 ]; then echo "ERROR: release build aborted — ${_release_errors} modified repo(s). Use BUILD_CHANNEL=dev for iteration builds." exit 1 diff --git a/docs/POUDRIERE-BUILD-SERVER.md b/docs/POUDRIERE-BUILD-SERVER.md index a30daa06..c6e3815a 100644 --- a/docs/POUDRIERE-BUILD-SERVER.md +++ b/docs/POUDRIERE-BUILD-SERVER.md @@ -162,31 +162,31 @@ poudriere jail -c -j clawdie-amd64 -v 15.0-RELEASE -a amd64 poudriere ports -c -p clawdie -m git -B main ``` -### 2.4 Create colibri port +### 2.4 Colibri port — canonical in the colibri repo -Create `sysutils/colibri/` in the ports tree: +The `sysutils/colibri` port is **owned by the colibri repo**, kept with the code +it builds at `packaging/freebsd/port/sysutils/colibri/`: ``` sysutils/colibri/ -├── Makefile -├── distinfo +├── Makefile USES=cargo; ships 6 binaries + rc.d services ├── pkg-descr -└── pkg-plist +├── pkg-plist +└── files/ rc.d templates (colibri_daemon.in, colibri_bridge.in) ``` -**Makefile** (Rust port pattern): +Copy that directory into the poudriere ports tree before building. **This repo +keeps no duplicate** — `build.sh`'s release gate fails if `ports/sysutils/colibri/` +reappears here. Key facts: -The port files are maintained in the **colibri** repo (source-of-truth, kept -with the code) at `packaging/freebsd/port/sysutils/colibri/` — `Makefile`, -`pkg-descr`, `pkg-plist`, plus a README covering generation. Don't hand-copy a -Makefile here; drop that directory into the ports tree. Key facts: - -- `LICENSE= AGPLv3` (per `colibri/Cargo.toml` — **not** MIT). +- `LICENSE= AGPLv3` (per `colibri/Cargo.toml`). - `USES= cargo`; source from the Forgejo archive (tagged `v${DISTVERSION}`). - Ships 6 binaries: `clawdie`, `colibri`, `colibri-daemon`, `colibri-mcp`, - `colibri-test-agent`, `colibri-tui`. -- `CARGO_CRATES` and `distinfo` are generated on the build host - (`make cargo-crates`, `make makesum`). + `colibri-test-agent`, `colibri-tui` — plus the `colibri_daemon` / `colibri_bridge` + rc.d services. +- `CARGO_CRATES` is committed and kept in sync with `Cargo.lock` by + `check-cargo-crates.sh` (colibri CI). `distinfo` is generated on the build host + with `make makesum`. ### 2.5 Build diff --git a/ports/sysutils/colibri/Makefile b/ports/sysutils/colibri/Makefile deleted file mode 100644 index fa7f9a12..00000000 --- a/ports/sysutils/colibri/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -PORTNAME= colibri -DISTVERSION= 0.0.1 -CATEGORIES= sysutils - -MAINTAINER= hello@clawdie.si -WWW= https://code.smilepowered.org/clawdie/colibri - -LICENSE= MIT -LICENSE_FILE= ${WRKSRC}/LICENSE - -USES= cargo -MASTER_SITES= https://code.smilepowered.org/clawdie/colibri/archive/ -DISTFILES= v${DISTVERSION}${EXTRACT_SUFX} -WRKSRC= ${WRKDIR}/colibri - -# Colibri ships multiple binaries; list the ones worth installing -CARGO_BUILD= no -CARGO_INSTALL= no - -do-install: - ${INSTALL_PROGRAM} ${CARGO_TARGET_DIR}/release/colibri-daemon \ - ${STAGEDIR}${PREFIX}/sbin/colibri-daemon - ${INSTALL_PROGRAM} ${CARGO_TARGET_DIR}/release/colibri \ - ${STAGEDIR}${PREFIX}/bin/colibri - ${INSTALL_PROGRAM} ${CARGO_TARGET_DIR}/release/colibri-mcp \ - ${STAGEDIR}${PREFIX}/bin/colibri-mcp - ${INSTALL_PROGRAM} ${CARGO_TARGET_DIR}/release/colibri-probe \ - ${STAGEDIR}${PREFIX}/bin/colibri-probe - ${INSTALL_PROGRAM} ${CARGO_TARGET_DIR}/release/colibri-runtime-inventory \ - ${STAGEDIR}${PREFIX}/bin/colibri-runtime-inventory - ${INSTALL_SCRIPT} ${FILESDIR}/colibri_bridge.in \ - ${STAGEDIR}${PREFIX}/etc/rc.d/colibri_bridge - ${INSTALL_SCRIPT} ${FILESDIR}/colibri_daemon.in \ - ${STAGEDIR}${PREFIX}/etc/rc.d/colibri_daemon - ${MKDIR} ${STAGEDIR}/var/db/colibri - ${MKDIR} ${STAGEDIR}/var/log/colibri - ${MKDIR} ${STAGEDIR}/var/run/colibri - -.include diff --git a/ports/sysutils/colibri/distinfo b/ports/sysutils/colibri/distinfo deleted file mode 100644 index 4df5a58c..00000000 --- a/ports/sysutils/colibri/distinfo +++ /dev/null @@ -1,3 +0,0 @@ -TIMESTAMP = 1750962512 -SHA256 (v0.0.1.tar.gz) = 4f6f1cb7bc4b31407e6be3d94be8fbf7228f079a4eabd0972653f7adf8c822c8 -SIZE (v0.0.1.tar.gz) = 5930482 diff --git a/ports/sysutils/colibri/files/colibri_bridge.in b/ports/sysutils/colibri/files/colibri_bridge.in deleted file mode 100644 index 465c414d..00000000 --- a/ports/sysutils/colibri/files/colibri_bridge.in +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/sh -# -# Colibri TCP bridge — FreeBSD rc.d service -# -# Bridges the colibri-daemon Unix socket to a TCP port on the Tailscale -# interface so remote hosts (debby, domedog) can reach the Colibri control -# plane through the mesh VPN. -# -# socat runs in the FOREGROUND — it does not self-daemonize. rc.d runs it -# under daemon(8), which backgrounds it, writes a pidfile, restarts on crash, -# and redirects stdout/stderr to a logfile. -# -# Setup (one-time, as root): -# cp /home/clawdie/ai/hermes-bsd/packaging/freebsd/colibri_bridge.in \ -# /usr/local/etc/rc.d/colibri_bridge -# chmod 555 /usr/local/etc/rc.d/colibri_bridge -# sysrc colibri_bridge_enable=YES -# -# Requires: -# - socat installed (pkg install socat) -# - colibri_daemon running (socket at /var/run/colibri/colibri.sock) -# - pf rule for the bridge port on tailscale0 - -# PROVIDE: colibri_bridge -# REQUIRE: LOGIN cleanvar colibri_daemon -# BEFORE: hermes_dashboard -# KEYWORD: shutdown - -. /etc/rc.subr - -name="colibri_bridge" -rcvar="colibri_bridge_enable" - -load_rc_config $name - -: ${colibri_bridge_enable:="NO"} -: ${colibri_bridge_user:="clawdie"} -: ${colibri_bridge_group:="clawdie"} -: ${colibri_bridge_listen_addr:="100.72.229.63"} -: ${colibri_bridge_listen_port:="9190"} -: ${colibri_bridge_socket:="/var/run/colibri/colibri.sock"} -: ${colibri_bridge_run_dir:="/var/run/colibri"} -: ${colibri_bridge_logfile:="/var/log/colibri/bridge.log"} -: ${colibri_bridge_socat:="/usr/local/bin/socat"} - -pidfile="${colibri_bridge_run_dir}/colibri-bridge.pid" -supervisor_pidfile="${colibri_bridge_run_dir}/colibri-bridge-supervisor.pid" - -command="/usr/sbin/daemon" -command_args="-P ${supervisor_pidfile} -p ${pidfile} -t ${name} \ - -o ${colibri_bridge_logfile} ${colibri_bridge_socat} \ - TCP-LISTEN:${colibri_bridge_listen_port},bind=${colibri_bridge_listen_addr},fork,reuseaddr \ - UNIX-CONNECT:${colibri_bridge_socket}" - -# procname must match the actual child process so rc.subr finds the right PID -# and doesn't kill the wrong daemon(8) child. -procname="socat" - -start_precmd="colibri_bridge_prestart" -stop_cmd="colibri_bridge_stop" -status_cmd="colibri_bridge_status" -extra_commands="health" - -colibri_bridge_prestart() -{ - install -d -o "${colibri_bridge_user}" -g "${colibri_bridge_group}" -m 0750 \ - "${colibri_bridge_run_dir}" - install -d -o "${colibri_bridge_user}" -g "${colibri_bridge_group}" -m 0750 \ - "$(/usr/bin/dirname "${colibri_bridge_logfile}")" - - rm -f "${pidfile}" "${supervisor_pidfile}" - - if [ ! -x "${colibri_bridge_socat}" ]; then - echo "ERROR: socat not found at ${colibri_bridge_socat}" - return 1 - fi - if [ ! -S "${colibri_bridge_socket}" ]; then - echo "ERROR: colibri socket not found at ${colibri_bridge_socket}" - echo " Start colibri_daemon first: service colibri_daemon start" - return 1 - fi -} - -colibri_bridge_stop() -{ - local _sup="" - [ -f "${supervisor_pidfile}" ] && _sup=$(cat "${supervisor_pidfile}" 2>/dev/null) - if [ -n "${_sup}" ] && kill -0 "${_sup}" 2>/dev/null; then - echo "Stopping ${name} (daemon(8) supervisor pid ${_sup})." - kill -TERM "${_sup}" 2>/dev/null - local _n=0 - while kill -0 "${_sup}" 2>/dev/null && [ ${_n} -lt 30 ]; do - sleep 1 - _n=$((_n + 1)) - done - if kill -0 "${_sup}" 2>/dev/null; then - echo "Supervisor did not exit in time; sending SIGKILL." - kill -KILL "${_sup}" 2>/dev/null - fi - else - echo "${name} is not running." - fi - local _ch="" - [ -f "${pidfile}" ] && _ch=$(cat "${pidfile}" 2>/dev/null) - if [ -n "${_ch}" ] && kill -0 "${_ch}" 2>/dev/null; then - kill -TERM "${_ch}" 2>/dev/null - fi - rm -f "${supervisor_pidfile}" "${pidfile}" -} - -health_cmd="colibri_bridge_health" -colibri_bridge_health() - -colibri_bridge_status() -{ -tcolibri_bridge_health -} -{ - if ! pgrep -f "socat.*${colibri_bridge_listen_port}" >/dev/null 2>&1; then - echo "colibri-bridge socat process not found" - return 1 - fi - - _resp=$(printf '{"cmd":"status"}\n' | \ - nc -w 2 "${colibri_bridge_listen_addr}" "${colibri_bridge_listen_port}" 2>/dev/null) - if [ -n "${_resp}" ]; then - echo "colibri-bridge is healthy (port ${colibri_bridge_listen_port} responding)" - return 0 - else - echo "colibri-bridge socat running but no response on port ${colibri_bridge_listen_port}" - return 1 - fi -} - -run_rc_command "$1" diff --git a/ports/sysutils/colibri/files/colibri_daemon.in b/ports/sysutils/colibri/files/colibri_daemon.in deleted file mode 100644 index 0dae534b..00000000 --- a/ports/sysutils/colibri/files/colibri_daemon.in +++ /dev/null @@ -1,198 +0,0 @@ -#!/bin/sh -# -# Colibri daemon — FreeBSD rc.d service -# -# colibri-daemon runs in the FOREGROUND — it does not self-daemonize or write a -# pidfile. rc.d runs it under daemon(8), which backgrounds it, writes the -# child pidfile (colibri-daemon PID), restarts on crash, and redirects -# stdout/stderr (tracing) to a logfile. rc.subr performs the privilege drop -# through ${name}_user. -# -# 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=YES # or NO during dual-run -# cp packaging/freebsd/provider.env.example /usr/local/etc/colibri/provider.env -# chmod 600 /usr/local/etc/colibri/provider.env -# $EDITOR /usr/local/etc/colibri/provider.env # fill in vault credentials -# -# Runtime: -# service colibri_daemon start -# service colibri_daemon status -# service colibri_daemon stop -# -# Requires: -# - colibri-daemon binary at /usr/local/bin/colibri-daemon -# - colibri user/group (privilege drop target) - -# PROVIDE: colibri_daemon -# REQUIRE: LOGIN cleanvar -# KEYWORD: shutdown - -. /etc/rc.subr - -name="colibri_daemon" -rcvar="colibri_daemon_enable" - -load_rc_config $name - -: ${colibri_daemon_enable:="NO"} -: ${colibri_daemon_user:="colibri"} -: ${colibri_daemon_group:="colibri"} -: ${colibri_daemon_binary:="/usr/local/bin/colibri-daemon"} -: ${colibri_daemon_data_dir:="/var/db/colibri"} -: ${colibri_daemon_run_dir:="/var/run/colibri"} -: ${colibri_daemon_socket:="${colibri_daemon_run_dir}/colibri.sock"} -: ${colibri_daemon_db_path:="${colibri_daemon_data_dir}/colibri.sqlite"} -: ${colibri_daemon_logfile:="/var/log/colibri/daemon.log"} -: ${colibri_daemon_provider_env:="/usr/local/etc/colibri/provider.env"} -: ${colibri_daemon_host:="$(/bin/hostname)"} -: ${colibri_daemon_cost_mode:="smart"} - -pidfile="${colibri_daemon_run_dir}/colibri-daemon.pid" -# Supervisor pidfile (the daemon(8) parent). Kept distinct from the child -# pidfile so `stop` can target the supervisor — see colibri_daemon_stop. -supervisor_pidfile="${colibri_daemon_run_dir}/colibri-daemon-supervisor.pid" - -# Run colibri-daemon under daemon(8): -# -P supervisor pidfile (the daemon(8) parent — used by stop) -# -p child pidfile (writes colibri-daemon PID — used by start/status) -# -r restart on crash, -t process title, -# -o append stdout/stderr to log. -# User selection is handled by rc.subr through colibri_daemon_user. -command="/usr/sbin/daemon" -command_args="-P ${supervisor_pidfile} -p ${pidfile} -r -t ${name} \ - -o ${colibri_daemon_logfile} ${colibri_daemon_binary}" - -# Use the child's process name so rc.subr can find the right process via the -# child pidfile. Using the daemon(8) supervisor path would collide with -# tailscaled and other daemon(8)-managed services on the system. -procname="colibri-daemon" - -start_precmd="colibri_daemon_prestart" -start_postcmd="colibri_daemon_poststart" -stop_cmd="colibri_daemon_stop" -stop_postcmd="colibri_daemon_poststop" -extra_commands="health" - -colibri_daemon_prestart() -{ - # /var/run is tmpfs on FreeBSD (wiped each boot) — recreate every start. - install -d -o "${colibri_daemon_user}" -g "${colibri_daemon_group}" -m 0750 \ - "${colibri_daemon_run_dir}" - install -d -o "${colibri_daemon_user}" -g "${colibri_daemon_group}" -m 0750 \ - "${colibri_daemon_data_dir}" - install -d -o "${colibri_daemon_user}" -g "${colibri_daemon_group}" -m 0750 \ - "$(/usr/bin/dirname "${colibri_daemon_logfile}")" - - # Clear stale pidfiles before starting so a previous hard-killed run does - # not trip rc.subr's "already running" check. The socket is left to the - # daemon: it removes a stale socket on bind but refuses to start if a live - # daemon is already listening, so an rc-side rm here could only clobber a - # running instance. - rm -f "${pidfile}" "${supervisor_pidfile}" - - # Provider keys are optional. Keep them in a root-owned env file instead of - # rc.conf so they are easy to rotate and not world-readable. - if [ -r "${colibri_daemon_provider_env}" ]; then - set -a - . "${colibri_daemon_provider_env}" - set +a - fi - - # Config is passed to the child via the environment. - export COLIBRI_DAEMON_DATA_DIR="${colibri_daemon_data_dir}" - export COLIBRI_DAEMON_SOCKET="${colibri_daemon_socket}" - export COLIBRI_DB_PATH="${colibri_daemon_db_path}" - export COLIBRI_HOST="${colibri_daemon_host}" - export COLIBRI_COST_MODE="${colibri_daemon_cost_mode}" - export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" -} - -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_stop() -{ - # daemon(8) -r restarts the child if it is killed directly, so a plain - # SIGTERM to the child pidfile would just be undone. Stop the supervisor - # instead: on SIGTERM it forwards the signal to the child and exits without - # restarting it. - local _sup="" - [ -f "${supervisor_pidfile}" ] && _sup=$(cat "${supervisor_pidfile}" 2>/dev/null) - if [ -n "${_sup}" ] && kill -0 "${_sup}" 2>/dev/null; then - echo "Stopping ${name} (daemon(8) supervisor pid ${_sup})." - kill -TERM "${_sup}" 2>/dev/null - local _n=0 - while kill -0 "${_sup}" 2>/dev/null && [ ${_n} -lt 30 ]; do - sleep 1 - _n=$((_n + 1)) - done - if kill -0 "${_sup}" 2>/dev/null; then - echo "Supervisor did not exit in time; sending SIGKILL." - kill -KILL "${_sup}" 2>/dev/null - fi - else - echo "${name} is not running." - fi - # Belt-and-suspenders: terminate the child if it somehow outlived the - # supervisor (e.g. supervisor SIGKILLed before it could clean up). - local _ch="" - [ -f "${pidfile}" ] && _ch=$(cat "${pidfile}" 2>/dev/null) - if [ -n "${_ch}" ] && kill -0 "${_ch}" 2>/dev/null; then - kill -TERM "${_ch}" 2>/dev/null - fi - rm -f "${supervisor_pidfile}" "${pidfile}" -} - -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" -status_cmd="colibri_daemon_status" - -colibri_daemon_health() -{ - if [ ! -S "${colibri_daemon_socket}" ]; then - echo "colibri-daemon socket not found" - return 1 - fi - - _resp=$(printf '{"cmd":"status"}\n' | nc -U "${colibri_daemon_socket}" -w 2 2>/dev/null) - if [ -n "${_resp}" ]; then - echo "colibri-daemon is healthy (socket responding)" - return 0 - else - echo "colibri-daemon socket exists but no response" - return 1 - fi -} - -colibri_daemon_status() -{ - colibri_daemon_health -} - -run_rc_command "$1" diff --git a/ports/sysutils/colibri/pkg-descr b/ports/sysutils/colibri/pkg-descr deleted file mode 100644 index 7044d524..00000000 --- a/ports/sysutils/colibri/pkg-descr +++ /dev/null @@ -1,3 +0,0 @@ -Colibri is the Clawdie control-plane core — a cross-platform Rust daemon -that provides agent supervision, a task board with capability-based routing, -SQLite-backed coordination, and MCP bridge support for editor integration. diff --git a/scripts/poudriere/README.md b/scripts/poudriere/README.md index 843e1864..69e46898 100644 --- a/scripts/poudriere/README.md +++ b/scripts/poudriere/README.md @@ -35,8 +35,12 @@ poudriere-setup.sh → poudriere-build.sh → serve + client config mdo -u root ./poudriere-build.sh --jail clawdie-amd64 --ports clawdie sysutils/colibri ``` - The `sysutils/colibri` port itself (Makefile/distinfo/pkg-plist) is created in the - ports tree per the runbook's Phase 2.4. + The `sysutils/colibri` port is the **canonical copy in the colibri repo** + (`packaging/freebsd/port/sysutils/colibri/`) — copy it into the poudriere ports + tree before building. This repo keeps no duplicate; the colibri repo owns the + port (its `Makefile`/`pkg-plist`/`CARGO_CRATES`/rc.d track Colibri's source, and + a `check-cargo-crates.sh` CI gate keeps it in sync). Generate `distinfo` on the + build host with `make makesum`. 3. **Serve + clients.** Point nginx at `/usr/local/poudriere/data/packages/clawdie-amd64-clawdie` for `https://pkg.clawdie.si/`