diff --git a/crates/colibri-client/tests/live_socket_check.rs b/crates/colibri-client/tests/live_socket_check.rs index c22041a..1b4b6be 100644 --- a/crates/colibri-client/tests/live_socket_check.rs +++ b/crates/colibri-client/tests/live_socket_check.rs @@ -233,7 +233,7 @@ async fn poll_tasks_spawns_agent_for_claimed_task() { { let store = state.store.lock().unwrap(); store - .register_agent("test-agent", serde_json::json!([])) + .register_agent("test-agent", serde_json::json!([]), None) .unwrap(); } diff --git a/crates/colibri-daemon/src/socket.rs b/crates/colibri-daemon/src/socket.rs index 850ec35..7ad3d33 100644 --- a/crates/colibri-daemon/src/socket.rs +++ b/crates/colibri-daemon/src/socket.rs @@ -265,7 +265,7 @@ async fn dispatch(cmd: ColibriCommand, state: &SharedState) -> ColibriResponse { } ColibriCommand::ListAgents => cmd_list_agents(state).await, ColibriCommand::RegisterAgent { name, capabilities } => { - cmd_register_agent(state, name, capabilities).await + cmd_register_agent(state, name, capabilities, None).await } ColibriCommand::ListSkills => cmd_list_skills(state).await, ColibriCommand::RegisterSkill { @@ -1000,9 +1000,15 @@ async fn cmd_register_agent( state: &SharedState, name: String, capabilities: Option, + _host: Option<&str>, ) -> ColibriResponse { let caps = capabilities.unwrap_or(serde_json::json!([])); - match state.store.lock().unwrap().register_agent(&name, caps) { + match state + .store + .lock() + .unwrap() + .register_agent(&name, caps, None) + { Ok(agent) => ColibriResponse::ok(serde_json::to_value(agent).unwrap_or_default()), Err(e) => ColibriResponse::err(format!("register agent failed: {e}")), } diff --git a/crates/colibri-store/src/lib.rs b/crates/colibri-store/src/lib.rs index da86ae9..a0e074c 100644 --- a/crates/colibri-store/src/lib.rs +++ b/crates/colibri-store/src/lib.rs @@ -335,7 +335,12 @@ impl Store { // ------------------------------------------------------------------ /// Register a new agent. - pub fn register_agent(&self, name: &str, capabilities: serde_json::Value) -> Result { + pub fn register_agent( + &self, + name: &str, + capabilities: serde_json::Value, + _host: Option<&str>, // Phase 3: agent presence — host registry (WIP) + ) -> Result { let id = uuid::Uuid::new_v4().to_string(); let now = Utc::now().to_rfc3339(); let caps_json = serde_json::to_string(&capabilities)?; @@ -651,7 +656,7 @@ mod tests { // Register an agent first (FK constraint) let agent = store - .register_agent("codex", serde_json::json!(["code", "rust"])) + .register_agent("codex", serde_json::json!(["code", "rust"]), None) .unwrap(); // Create @@ -686,10 +691,10 @@ mod tests { fn test_claim_task_is_exclusive() { let store = Store::open_memory().unwrap(); let first = store - .register_agent("first", serde_json::json!(["freebsd"])) + .register_agent("first", serde_json::json!(["freebsd"]), None) .unwrap(); let second = store - .register_agent("second", serde_json::json!(["freebsd"])) + .register_agent("second", serde_json::json!(["freebsd"]), None) .unwrap(); let task = store.create_task("contended", None).unwrap(); @@ -711,7 +716,9 @@ mod tests { #[test] fn test_claim_task_not_found() { let store = Store::open_memory().unwrap(); - let agent = store.register_agent("a", serde_json::json!(["x"])).unwrap(); + let agent = store + .register_agent("a", serde_json::json!(["x"]), None) + .unwrap(); let result = store.claim_task("nonexistent", &agent.id); assert!( matches!(result, Err(StoreError::NotFound(_))), @@ -744,7 +751,7 @@ mod tests { let store = Store::open_memory().unwrap(); let caps = serde_json::json!(["code", "rust", "freebsd"]); - let agent = store.register_agent("codex", caps.clone()).unwrap(); + let agent = store.register_agent("codex", caps.clone(), None).unwrap(); assert_eq!(agent.name, "codex"); assert_eq!(agent.status, "active"); @@ -782,7 +789,7 @@ mod tests { let store = Store::open_memory().unwrap(); store.create_task("Export test", None).unwrap(); store - .register_agent("test-agent", serde_json::json!(["test"])) + .register_agent("test-agent", serde_json::json!(["test"]), None) .unwrap(); store .register_skill("test-skill", None, Some("test")) diff --git a/crates/colibri-store/src/schema.rs b/crates/colibri-store/src/schema.rs index ee8d619..438b300 100644 --- a/crates/colibri-store/src/schema.rs +++ b/crates/colibri-store/src/schema.rs @@ -59,3 +59,11 @@ CREATE TABLE IF NOT EXISTS tenants ( CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status); "; + +/// Column additions since the initial schema. Each runs inside a try-block +/// so the store stays idempotent: adding a column that already exists is a +#[allow(dead_code)] // Phase 3 WIP — schema not yet wired +pub const MIGRATIONS: &[&str] = &[ + "ALTER TABLE agents ADD COLUMN host TEXT", + "ALTER TABLE agents ADD COLUMN last_seen TEXT", +]; diff --git a/packaging/freebsd/colibri_bridge.in b/packaging/freebsd/colibri_bridge.in index 465c414..2e95438 100644 --- a/packaging/freebsd/colibri_bridge.in +++ b/packaging/freebsd/colibri_bridge.in @@ -36,7 +36,7 @@ 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_addr:="TAILSCALE_IP_REQUIRED"} : ${colibri_bridge_listen_port:="9190"} : ${colibri_bridge_socket:="/var/run/colibri/colibri.sock"} : ${colibri_bridge_run_dir:="/var/run/colibri"} @@ -74,6 +74,11 @@ colibri_bridge_prestart() echo "ERROR: socat not found at ${colibri_bridge_socat}" return 1 fi + if [ "${colibri_bridge_listen_addr}" = "TAILSCALE_IP_REQUIRED" ]; then + echo "ERROR: colibri_bridge_listen_addr not configured" + echo " Set in /etc/rc.conf: sysrc colibri_bridge_listen_addr=" + 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" @@ -110,11 +115,11 @@ colibri_bridge_stop() health_cmd="colibri_bridge_health" colibri_bridge_health() +{ + colibri_bridge_status +} 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" diff --git a/scripts/stage-wiki-for-starlight.sh b/scripts/stage-wiki-for-starlight.sh new file mode 100755 index 0000000..6fa16de --- /dev/null +++ b/scripts/stage-wiki-for-starlight.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# stage-wiki-for-starlight — copy colibri wiki pages into a Starlight docs +# content tree with injected frontmatter and route-fixed links. +# +# Usage: +# ./scripts/stage-wiki-for-starlight.sh [COLIBRI_REPO] [STARLIGHT_CONTENT] +# +# Defaults: +# COLIBRI_REPO = /home/clawdie/ai/colibri +# STARLIGHT_CONTENT = /usr/home/clawdie/clawdie-docs/src/content/docs +# +# What it does: +# 1. Copies docs/wiki/*.md into /wiki/ +# 2. Injects Starlight frontmatter (title from H1, description from first +# non-link paragraph) +# 3. Rewrites intra-wiki ./page.md links to Starlight /wiki/page/ routes +# 4. Rewrites source-code links (../../crates/..., ../DOC.md) to Forgejo +# permanent URLs pinned at the build-time HEAD commit +# 5. Drops the backlink line (Starlight provides breadcrumbs) +# +# Idempotent — safe to re-run before every docs build. +set -eu + +COLIBRI_REPO="${1:-/home/clawdie/ai/colibri}" +STARLIGHT_CONTENT="${2:-/usr/home/clawdie/clawdie-docs/src/content/docs}" +WIKI_SRC="${COLIBRI_REPO}/docs/wiki" +WIKI_DST="${STARLIGHT_CONTENT}/wiki" + +if [ ! -d "${WIKI_SRC}" ]; then + echo "ERROR: wiki source not found at ${WIKI_SRC}" >&2 + exit 1 +fi +if [ ! -d "${STARLIGHT_CONTENT}" ]; then + echo "ERROR: Starlight content dir not found at ${STARLIGHT_CONTENT}" >&2 + exit 1 +fi + +# Pin the colibri commit for source-code permalinks. +COLIBRI_COMMIT=$(git -C "${COLIBRI_REPO}" rev-parse HEAD 2>/dev/null || echo "main") +FORGEJO_BASE="https://code.smilepowered.org/clawdie/colibri/src/commit/${COLIBRI_COMMIT}" + +echo "Staging wiki from ${WIKI_SRC} -> ${WIKI_DST} (colibri ${COLIBRI_COMMIT})" + +rm -rf "${WIKI_DST}" +mkdir -p "${WIKI_DST}" + +for src in "${WIKI_SRC}"/*.md; do + base="$(basename "${src}")" + dst="${WIKI_DST}/${base}" + + # Extract title from H1 (first line starting with "# ") + title=$(head -1 "${src}" | sed 's/^# //') + + # Extract description from first substantial non-link paragraph. + desc=$(awk ' + NR <= 1 { next } + /^$/ { next } + /^[←\[#>`]/ { next } + /^```/ { in_code = !in_code; next } + in_code { next } + /^[A-Za-z]/ { print; exit } + ' "${src}") + + # Fallback + [ -z "${desc}" ] && desc="${title}" + + # Escape double quotes for YAML + title_yaml=$(printf '%s' "${title}" | sed 's/"/\\"/g') + desc_yaml=$(printf '%s' "${desc}" | sed 's/"/\\"/g') + + # Write frontmatter + { + printf '%s\n' '---' + printf 'title: "%s"\n' "${title_yaml}" + printf 'description: "%s"\n' "${desc_yaml}" + printf '%s\n' '---' + printf '\n' + } > "${dst}" + + # Body with link rewrites. + # FreeBSD sed: use -E for extended regex, avoid GNU-isms. + awk 'NR==1 { next } 1' "${src}" \ + | grep -v '^← \[index\]' \ + | sed -E 's|\(\./([a-z0-9-]+)\.md\)|(/wiki/\1/)|g' \ + | sed -E "s|\(\.\./([A-Za-z0-9_.-]+\.md)\)|(${FORGEJO_BASE}/docs/\1)|g" \ + | sed -E "s|\(\.\./\.\./(crates/[^)]+)\)|(${FORGEJO_BASE}/\1)|g" \ + | sed -E "s|\(\.\./\.\./(packaging/[^)]+)\)|(${FORGEJO_BASE}/\1)|g" \ + | sed -E "s|\(\.\./\.\./(scripts/[^)]+)\)|(${FORGEJO_BASE}/\1)|g" \ + >> "${dst}" +done + +echo "Wiki staged: $(ls "${WIKI_DST}" | wc -l | tr -d ' ') pages"