Phase 3 agent presence + bridge IP scrub & health-fn fix #204

Merged
clawdie merged 4 commits from fix/phase3-rebase-callers into main 2026-06-26 01:34:26 +02:00
6 changed files with 132 additions and 14 deletions

View file

@ -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();
}

View file

@ -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<serde_json::Value>,
_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}")),
}

View file

@ -335,7 +335,12 @@ impl Store {
// ------------------------------------------------------------------
/// Register a new agent.
pub fn register_agent(&self, name: &str, capabilities: serde_json::Value) -> Result<Agent> {
pub fn register_agent(
&self,
name: &str,
capabilities: serde_json::Value,
_host: Option<&str>, // Phase 3: agent presence — host registry (WIP)
) -> Result<Agent> {
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"))

View file

@ -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",
];

View file

@ -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=<tailscale-ip>"
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"

View file

@ -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 <STARLIGHT_CONTENT>/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"