0.12.0: hw-probe + model fixes + mother MCP infra

Combined from three feature branches:
- feature/hw-probe-agent-bootstrap: JSON hardware probe (clawdie-hw-probe),
  remove desktop icon, update START-HERE.txt
- chore/0.12.0-model-fix-bump: deepseek-v4-pro model names, version 0.12.0
- feature/mother-mcp-infra: build-colibri.sh MCP tool, colibri-mcp-ssh wrapper
This commit is contained in:
Sam & Claude 2026-06-23 10:49:38 +02:00
parent bbf154b571
commit 7300fec1e2
10 changed files with 369 additions and 23 deletions

View file

@ -216,8 +216,6 @@ test -x /mnt/usr/local/bin/startx
test -x /mnt/usr/local/bin/clawdie-startx
test -x /mnt/usr/local/bin/clawdie-gui
test -x /mnt/usr/local/bin/hw-report
test -f "/mnt/usr/local/share/applications/Clawdie Hardware Report.desktop"
test -f "/mnt/home/clawdie/Desktop/Clawdie Hardware Report.desktop"
test -x /mnt/home/clawdie/.xinitrc
test -x /mnt/home/clawdie/.config/xfce4/xinitrc
pkg -r /mnt info -e xterm

View file

@ -13,7 +13,7 @@ FREEBSD_MEMSTICK_SHA256_URL="${FREEBSD_ISO_BASE_URL}/CHECKSUM.SHA256-FreeBSD-${F
# clawdie-iso and colibri share this unified version (keep colibri's Cargo.toml
# in sync). zot and clawdie-ai keep their own versions, recorded as provenance in
# build-manifest.json. Bump this for each release.
ISO_VERSION="${ISO_VERSION:-0.11.0}"
ISO_VERSION="${ISO_VERSION:-0.12.0}"
BUILD_CHANNEL="${BUILD_CHANNEL:-dev}" # dev | release
# Output image

View file

@ -1308,6 +1308,8 @@ configure_live_operator_session() {
"${MOUNT_POINT}/usr/local/bin/clawdie-noblank-guard.sh"
install -m 0755 "${LIVE_SESSION_DIR}/hw-report" \
"${MOUNT_POINT}/usr/local/bin/hw-report"
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-hw-probe" \
"${MOUNT_POINT}/usr/local/bin/clawdie-hw-probe"
# Vaultwarden secret bridge (bw -> .env). Always staged: needs only the
# bundled `bw` CLI and a 0600 bootstrap drop; absent bootstrap = no-op, so
# the manual setup wizard stays the floor. See docs/VAULTWARDEN-SETUP.md.
@ -1711,15 +1713,11 @@ EOF
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Start Here.desktop"
install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
"${MOUNT_POINT}/usr/local/share/applications/Colibri Dashboard.desktop"
install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Hardware Report.desktop"
mkdir -p "${MOUNT_POINT}/home/clawdie/Desktop"
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop"
install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
"${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop"
install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop"
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso"
install -m 0644 "${LIVE_SESSION_DIR}/START-HERE.txt" \
"${MOUNT_POINT}/usr/local/share/clawdie-iso/START-HERE.txt"
@ -1756,8 +1754,7 @@ EOF
chmod 0755 "${MOUNT_POINT}/home/clawdie/Desktop"
chmod 0644 \
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop" \
"${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop" \
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop"
"${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop"
install_live_ai_source_snapshots

View file

@ -9,7 +9,7 @@ FIRST ACTIONS
1. Save any notes you want in this file or close it.
2. Open "Colibri Dashboard" on the desktop when you are ready.
3. Use "Clawdie Hardware Report" if this machine needs diagnostics.
3. Run 'mdo -u root hw-report' in a terminal if this machine needs diagnostics.
COLIBRI
-------
@ -75,9 +75,6 @@ Open manually:
HARDWARE REPORT
---------------
Desktop launcher:
Clawdie Hardware Report
Terminal:
mdo -u root hw-report

View file

@ -0,0 +1,288 @@
#!/bin/sh
# clawdie-hw-probe — single-shot JSON hardware probe
#
# Outputs one JSON object to stdout. All messages/errors go to stderr.
# Designed to run as root for full hardware access.
# Target: < 3 seconds on cold cache.
#
# Schema: clawdie.hw-probe.v1
set -u
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH
########################################################################
# helpers
########################################################################
die() {
printf '{"error":"%s","schema_version":"clawdie.hw-probe.v1"}\n' "$(json_escape "$1")"
exit 1
}
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g; s/\r//g; s/\t/\\t/g'
}
json_bool() {
if [ "$1" = "yes" ] || [ "$1" = "YES" ] || [ "$1" = "true" ] || [ "$1" = "TRUE" ] || [ "$1" = "1" ]; then
printf 'true'
else
printf 'false'
fi
}
########################################################################
# field collectors — each function prints to a temp file or variable
########################################################################
collect_hostname() {
hostname 2>/dev/null || echo "unknown"
}
collect_freebsd_version() {
# kernel, userland, runtime
_k="$(freebsd-version -k 2>/dev/null || echo "unknown")"
_u="$(freebsd-version -u 2>/dev/null || echo "unknown")"
_r="$(freebsd-version -r 2>/dev/null || echo "unknown")"
printf '{"kernel":"%s","userland":"%s","runtime":"%s"}' \
"$(json_escape "$_k")" \
"$(json_escape "$_u")" \
"$(json_escape "$_r")"
}
collect_ram_gb() {
_bytes="$(sysctl -n hw.physmem 2>/dev/null || echo 0)"
# Round to nearest GB: add 0.5 GB then truncate
_half_gb=536870912 # 2^29
_gb=$(( ( _bytes + _half_gb ) / 1073741824 ))
printf '%d' "$_gb"
}
collect_cpu_model() {
sysctl -n hw.model 2>/dev/null || echo "unknown"
}
collect_cpu_cores() {
sysctl -n hw.ncpu 2>/dev/null || echo "0"
}
collect_gpu() {
# Extract VGA/display devices from pciconf -lv
# Output: JSON array of {vendor, device, chip}
_found=0
printf '['
pciconf -lv 2>/dev/null | awk '
BEGIN { in_block=0; vendor=""; device=""; chip=""; first=1 }
/^[a-z]/ {
if (in_block && device ~ /VGA|display/) {
if (!first) printf ",";
gsub(/"/, "\\\"", vendor);
gsub(/"/, "\\\"", device);
gsub(/"/, "\\\"", chip);
printf "{\"vendor\":\"%s\",\"device\":\"%s\",\"chip\":\"%s\"}", vendor, device, chip;
first=0;
}
in_block=1; vendor=""; device=""; chip=$0;
sub(/@.*/, "", chip);
}
/vendor *=/ { vendor=$0; sub(/^[[:space:]]*vendor[[:space:]]*=[[:space:]]*/, "", vendor); sub(/'\''/, "", vendor); sub(/'\''$/, "", vendor) }
/device *=/ { device=$0; sub(/^[[:space:]]*device[[:space:]]*=[[:space:]]*/, "", device); sub(/'\''/, "", device); sub(/'\''$/, "", device) }
END {
if (in_block && device ~ /VGA|display/) {
if (!first) printf ",";
gsub(/"/, "\\\"", vendor);
gsub(/"/, "\\\"", device);
gsub(/"/, "\\\"", chip);
printf "{\"vendor\":\"%s\",\"device\":\"%s\",\"chip\":\"%s\"}", vendor, device, chip;
}
}
'
printf ']'
}
collect_gpu_driver() {
# Check which KMS drivers are loaded
printf '['
_first=1
for _drv in amdgpu i915kms nvidia-modeset; do
if kldstat -q -n "$_drv" 2>/dev/null; then
if [ "$_first" -ne 1 ]; then printf ','; fi
printf '"%s"' "$_drv"
_first=0
fi
done
printf ']'
}
collect_disks() {
# Disk names + sizes
printf '{'
_disks="$(sysctl -n kern.disks 2>/dev/null || true)"
_first=1
for _disk in $_disks; do
_size=""
# Try geom disk list first (fast, no device open)
if command -v geom >/dev/null 2>&1; then
_size="$(geom disk list "$_disk" 2>/dev/null | awk '/Mediasize:/ {print $2}' | head -1)"
fi
# Fallback to camcontrol
if [ -z "$_size" ] && command -v camcontrol >/dev/null 2>&1; then
_size="$(camcontrol identify "/dev/${_disk}" 2>/dev/null | awk -F'[<>]' '/LBA48 supported|LBA supported/{print $2}' | head -1)"
fi
# Fallback to diskinfo
if [ -z "$_size" ] && command -v diskinfo >/dev/null 2>&1; then
_size="$(diskinfo "/dev/${_disk}" 2>/dev/null | awk '{print $3}' | head -1)"
fi
if [ "$_first" -ne 1 ]; then printf ','; fi
printf '"%s":%s' "$(json_escape "$_disk")" "${_size:-0}"
_first=0
done
printf '}'
}
collect_network_interfaces() {
# Simpler, reliable version that doesn't rely on subshell variable leakage
_ifaces="$(ifconfig -l 2>/dev/null)"
printf '{'
_first=1
for _iface in $_ifaces; do
_mac="$(ifconfig "$_iface" 2>/dev/null | awk '/ether/{print $2}' | head -1)"
[ -z "$_mac" ] && continue
if [ "$_first" -ne 1 ]; then printf ','; fi
printf '"%s":"%s"' "$(json_escape "$_iface")" "$(json_escape "$_mac")"
_first=0
done
printf '}'
}
collect_wifi() {
_wlan="$(sysctl -n net.wlan.devices 2>/dev/null || true)"
if [ -n "$_wlan" ]; then
printf '['
_first=1
for _dev in $_wlan; do
if [ "$_first" -ne 1 ]; then printf ','; fi
printf '"%s"' "$(json_escape "$_dev")"
_first=0
done
printf ']'
else
printf '[]'
fi
}
collect_usb_devices() {
if command -v usbconfig >/dev/null 2>&1; then
printf '%d' "$(usbconfig 2>/dev/null | grep -c '^ugen' || echo 0)"
else
printf '0'
fi
}
collect_xorg_running() {
if pgrep -x Xorg >/dev/null 2>&1; then
printf 'true'
else
printf 'false'
fi
}
collect_vulkan_support() {
if command -v vulkaninfo >/dev/null 2>&1 && \
vulkaninfo --summary >/dev/null 2>&1; then
printf 'true'
else
printf 'false'
fi
}
collect_boot_type() {
if [ -e /dev/da0 ]; then
printf '"usb"'
elif [ -e /dev/nda0 ] || [ -e /dev/nvd0 ]; then
printf '"nvme"'
elif [ -e /dev/ada0 ]; then
printf '"sata"'
else
printf '"unknown"'
fi
}
collect_colibri_running() {
if command -v service >/dev/null 2>&1; then
# service status can hang on rc.d scripts; enforce a 1s deadline
_status="$(timeout 1 service colibri_daemon status 2>/dev/null || echo "not_running")"
case "$_status" in
*"is running"*|*"Running"*)
printf 'true'
;;
*)
printf 'false'
;;
esac
else
printf 'false'
fi
}
collect_zfs_pools() {
if command -v zpool >/dev/null 2>&1; then
_pools="$(zpool list -H -o name 2>/dev/null || true)"
if [ -n "$_pools" ]; then
printf '['
_first=1
for _pool in $_pools; do
if [ "$_first" -ne 1 ]; then printf ','; fi
printf '"%s"' "$(json_escape "$_pool")"
_first=0
done
printf ']'
return
fi
fi
printf '[]'
}
########################################################################
# assemble JSON
########################################################################
_hostname="$(collect_hostname)"
_freebsd_ver="$(collect_freebsd_version)"
_ram_gb="$(collect_ram_gb)"
_cpu_model="$(collect_cpu_model)"
_cpu_cores="$(collect_cpu_cores)"
_gpu="$(collect_gpu)"
_gpu_driver="$(collect_gpu_driver)"
_disks="$(collect_disks)"
_net="$(collect_network_interfaces)"
_wifi="$(collect_wifi)"
_usb="$(collect_usb_devices)"
_xorg="$(collect_xorg_running)"
_vulkan="$(collect_vulkan_support)"
_boot_type="$(collect_boot_type)"
_colibri="$(collect_colibri_running)"
_zfs_pools="$(collect_zfs_pools)"
printf '{\n'
printf ' "schema_version":"clawdie.hw-probe.v1",\n'
printf ' "hostname":"%s",\n' "$(json_escape "$_hostname")"
printf ' "freebsd_version":%s,\n' "$_freebsd_ver"
printf ' "ram_gb":%s,\n' "$_ram_gb"
printf ' "cpu_model":"%s",\n' "$(json_escape "$_cpu_model")"
printf ' "cpu_cores":%s,\n' "$_cpu_cores"
printf ' "gpu":%s,\n' "$_gpu"
printf ' "gpu_driver":%s,\n' "$_gpu_driver"
printf ' "disks":%s,\n' "$_disks"
printf ' "network_interfaces":%s,\n' "$_net"
printf ' "wifi":%s,\n' "$_wifi"
printf ' "usb_devices":%s,\n' "$_usb"
printf ' "xorg_running":%s,\n' "$_xorg"
printf ' "vulkan_support":%s,\n' "$_vulkan"
printf ' "boot_type":%s,\n' "$_boot_type"
printf ' "colibri_running":%s,\n' "$_colibri"
printf ' "zfs_pools":%s\n' "$_zfs_pools"
printf '}\n'
exit 0

View file

@ -67,7 +67,7 @@ Inside it, any of these are honored:
/<agent>/harness.toml Which agent harness to run + basic knobs:
harness = "zot" # zot | pi | local
model = "claude-opus-4-8"
model = "deepseek-v4-pro"
cost_mode = "smart"
`harness` must be one of zot, pi, local

View file

@ -1,8 +0,0 @@
[Desktop Entry]
Type=Application
Name=Clawdie Hardware Report
Comment=Collect local hardware diagnostics for support and custom ISO builds
Exec=xfce4-terminal --hold --command "mdo -u root /usr/local/bin/hw-report"
Icon=utilities-system-monitor
Terminal=false
Categories=System;Utility;

View file

@ -0,0 +1,68 @@
#!/bin/sh
# build-colibri MCP tool — build a colibri crate from any git branch.
#
# Accepts a JSON-RPC tools/call request on stdin, builds the requested
# crate, and returns the result as a JSON content block to stdout.
#
# Expected input (MCP tools/call):
# {"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"build_colibri","arguments":{"crate":"colibri-daemon","branch":"main","features":"","release":"true"}}}
#
# Parameters:
# crate — workspace member to build (default: colibri-daemon)
# branch — git branch/ref to check out (default: main)
# features — optional comma-separated features
# release — "true" for --release, anything else for debug (default: true)
#
# Output: MCP JSON-RPC response with text content block
# {"success":true,"binary":"/home/clawdie/ai/colibri/target/release/colibri-daemon","duration_s":25.4,"branch":"main","commit":"abc1234"}
# or
# {"success":false,"error":"build failed: ..."}
set -eu
REPO="/home/clawdie/ai/colibri"
CRATE_DEFAULT="colibri-daemon"
BRANCH_DEFAULT="main"
# Read JSON from stdin, extract arguments
INPUT=$(cat)
CRATE=$(echo "$INPUT" | jq -r '.params.arguments.crate // "'"$CRATE_DEFAULT"'"')
BRANCH=$(echo "$INPUT" | jq -r '.params.arguments.branch // "'"$BRANCH_DEFAULT"'"')
FEATURES=$(echo "$INPUT" | jq -r '.params.arguments.features // ""')
RELEASE=$(echo "$INPUT" | jq -r '.params.arguments.release // "true"')
ID=$(echo "$INPUT" | jq -r '.id // "1"')
# Build flags
CARGO_FLAGS=""
if [ "$RELEASE" = "true" ]; then
CARGO_FLAGS="--release"
fi
if [ -n "$FEATURES" ]; then
CARGO_FLAGS="$CARGO_FLAGS --features $FEATURES"
fi
START_TS=$(date +%s)
# Fetch and checkout
cd "$REPO"
git fetch origin 2>&1 || true
if ! git checkout "$BRANCH" 2>&1; then
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"git checkout failed: %s"}}\n' "$ID" "$BRANCH"
exit 1
fi
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
# Build
BUILD_LOG=$(mktemp)
if cargo build $CARGO_FLAGS -p "$CRATE" >"$BUILD_LOG" 2>&1; then
DURATION=$(( $(date +%s) - START_TS ))
BINARY=$(find target -maxdepth 3 -type f -name "$CRATE" -perm -111 | grep -v deps | head -1)
SIZE=$(stat -f '%z' "$BINARY" 2>/dev/null || echo "0")
cat "$BUILD_LOG" | tail -5 >&2 # send tail to stderr for logging
printf '{"jsonrpc":"2.0","id":%s,"result":{"content":[{"type":"text","text":"{\\"success\\":true,\\"binary\\":\\"%s\\",\\"duration_s\\":%s,\\"branch\\":\\"%s\\",\\"commit\\":\\"%s\\",\\"size_bytes\\":%s}"}]}}\n' \
"$ID" "$BINARY" "$DURATION" "$BRANCH" "$COMMIT" "$SIZE"
else
DURATION=$(( $(date +%s) - START_TS ))
ERROR=$(tail -20 "$BUILD_LOG" | head -10 | tr '\n' ' ' | sed 's/"/\\"/g')
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"build failed in %ss: %s"}}\n' "$ID" "$DURATION" "$ERROR"
fi
rm -f "$BUILD_LOG"

View file

@ -0,0 +1,6 @@
#!/bin/sh
# Wrapper for SSH forced-command MCP entrypoint.
# SSH's command="..." restriction replaces the client's command with
# this script and stores the original in $SSH_ORIGINAL_COMMAND.
# Pass it through to colibri-mcp so `ssh colibri@mother tools` works.
exec /usr/local/bin/colibri-mcp ${SSH_ORIGINAL_COMMAND}

View file

@ -153,7 +153,7 @@ BW_SERVER="https://vault.smilepowered.org"
#
# Optional endpoints/models:
# DEEPSEEK_ENDPOINT="https://api.deepseek.com/chat/completions"
# DEEPSEEK_MODEL="deepseek-chat"
# DEEPSEEK_MODEL="deepseek-v4-pro"
#
# Behavior toggles (non-secret):
# COLIBRI_AUTOSPAWN_PI="YES" # auto-spawn one Pi on daemon startup