#!/bin/sh # Layer 2 — mother MCP boundary test (runs on Linux; no osa, no PostgreSQL). # # Proves two things the mother relies on, without standing up a real mother: # 2a) the colibri-mcp-ssh forced-command ALLOWLIST: "" and "tools" route to # colibri-mcp; everything else is rejected (exit 1, JSON error). Tested # both directly and through a real loopback sshd with command="..." forced. # 2b) the MCP handshake: `colibri-mcp tools` and the stdio tools/list JSON-RPC # return the Colibri tool catalog. # # The DB-backed node_register path needs real PostgreSQL and belongs on osa # (domedog is Docker-free). This layer stops at the SSH + MCP plumbing. # # Requires a prebuilt colibri-mcp. Point at it with COLIBRI_MCP_BIN, else the # colibri debug build is auto-detected. set -u SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) WRAPPER="${WRAPPER:-${SCRIPT_DIR}/../../colibri/packaging/mother/colibri-mcp-ssh}" # auto-detect the prebuilt binary if [ -z "${COLIBRI_MCP_BIN:-}" ]; then for c in "${SCRIPT_DIR}/../../colibri/target/debug/colibri-mcp" \ "${SCRIPT_DIR}/../../colibri/target/release/colibri-mcp"; do [ -x "$c" ] && COLIBRI_MCP_BIN="$c" && break done fi COLIBRI_MCP_BIN="${COLIBRI_MCP_BIN:-}" # colibri is a sibling repo; if it isn't checked out + built, skip cleanly so a # standalone clawdie-iso run is not a false failure. Force-fail with STRICT=1. if [ ! -r "${WRAPPER}" ] || [ -z "${COLIBRI_MCP_BIN}" ] || [ ! -x "${COLIBRI_MCP_BIN}" ]; then echo "SKIP: colibri not available (needs sibling colibri checkout + built colibri-mcp)." echo " set COLIBRI_MCP_BIN / WRAPPER, or STRICT=1 to make this a failure." [ -n "${STRICT:-}" ] && exit 2 exit 0 fi COLIBRI_MCP_BIN=$(CDPATH= cd -- "$(dirname -- "${COLIBRI_MCP_BIN}")" && pwd)/$(basename -- "${COLIBRI_MCP_BIN}") WORK=$(mktemp -d "${TMPDIR:-/tmp}/mcp-boundary.XXXXXX") || exit 2 SSHD_PID="" cleanup() { [ -n "${SSHD_PID}" ] && kill "${SSHD_PID}" 2>/dev/null; rm -rf "${WORK}"; } trap cleanup EXIT INT TERM # Test wrapper: identical allowlist logic, but the absolute /usr/local/bin path # is redirected to the prebuilt binary so we can run unprivileged. TWRAP="${WORK}/colibri-mcp-ssh" sed "s#/usr/local/bin/colibri-mcp#${COLIBRI_MCP_BIN}#g" "${WRAPPER}" >"${TWRAP}" chmod +x "${TWRAP}" PASS=0; FAIL=0; SKIP=0 ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } skip() { SKIP=$((SKIP+1)); printf ' SKIP %s\n' "$1"; } check(){ [ "$1" -eq 0 ] && ok "$2" || bad "$2"; } # check <0|1-result> echo "== 2b: MCP handshake (direct) ==" timeout 10 "${COLIBRI_MCP_BIN}" tools 2>/dev/null | grep -q 'colibri_status' check $? "colibri-mcp tools lists colibri_status" printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \ | timeout 10 "${COLIBRI_MCP_BIN}" 2>/dev/null | grep -q '"colibri_intake_task"' check $? "stdio tools/list returns colibri_intake_task" echo "== 2a: forced-command allowlist (direct wrapper) ==" # allowed: "tools" -> tool list out=$(SSH_ORIGINAL_COMMAND="tools" timeout 10 sh "${TWRAP}" 2>/dev/null) { echo "${out}" | grep -q 'colibri_status'; } ; check $? 'SSH_ORIGINAL_COMMAND="tools" -> tool list' # allowed: "" -> stdio MCP (feed a request, expect a JSON-RPC result) out=$(printf '%s\n' '{"jsonrpc":"2.0","id":7,"method":"tools/list"}' \ | SSH_ORIGINAL_COMMAND="" timeout 10 sh "${TWRAP}" 2>/dev/null) { echo "${out}" | grep -q '"id":7'; } ; check $? 'SSH_ORIGINAL_COMMAND="" -> stdio MCP responds' # rejected: arbitrary command must NOT run; exit 1 + JSON error on stderr for evil in 'rm -rf /' 'status' 'tools; rm -rf /' 'tools --help' '/bin/sh'; do err=$(SSH_ORIGINAL_COMMAND="${evil}" sh "${TWRAP}" 2>&1 >/dev/null); rc=$? { [ "${rc}" -eq 1 ] && echo "${err}" | grep -q 'rejected'; } check $? "rejected: '${evil}' (exit 1 + json error)" done echo "== 2a: forced-command through a REAL loopback sshd ==" if ! command -v sshd >/dev/null 2>&1 || ! command -v ssh-keygen >/dev/null 2>&1; then skip "sshd/ssh-keygen unavailable — direct wrapper test stands in" else HK="${WORK}/hostkey"; CK="${WORK}/clientkey"; AK="${WORK}/authorized_keys" ssh-keygen -t ed25519 -N '' -f "${HK}" >/dev/null 2>&1 ssh-keygen -t ed25519 -N '' -f "${CK}" >/dev/null 2>&1 # force every connection through the wrapper, exactly like mother's authorized_keys printf 'command="%s",restrict %s\n' "${TWRAP}" "$(cat "${CK}.pub")" >"${AK}" chmod 600 "${AK}" PORT=$(( (RANDOM % 5000) + 60000 )) SSHDBIN=$(command -v sshd) # Own config via -f so sshd ignores /etc/ssh + its (root-only) drop-ins. CFG="${WORK}/sshd_config" cat >"${CFG}" <"${WORK}/sshd.log" 2>&1 & SSHD_PID=$! sleep 1 if ! kill -0 "${SSHD_PID}" 2>/dev/null; then skip "sshd failed to start (see log) — direct wrapper test stands in" sed 's/^/ sshd: /' "${WORK}/sshd.log" 2>/dev/null | head -3 else SSHOPTS="-i ${CK} -p ${PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" # allowed via forced command # shellcheck disable=SC2086 ssh ${SSHOPTS} localhost tools 2>/dev/null | grep -q 'colibri_status' check $? "ssh ... tools -> tool list (forced command)" # rejected via forced command # shellcheck disable=SC2086 out=$(ssh ${SSHOPTS} localhost 'rm -rf /' 2>&1); rc=$? { [ "${rc}" -ne 0 ] && echo "${out}" | grep -q 'rejected'; } check $? "ssh ... 'rm -rf /' -> rejected (forced command)" fi fi echo echo "RESULT: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped" [ "${FAIL}" -eq 0 ] || exit 1 exit 0