feat(seed): OOTB zot seed — AGENTS.md→ZOT_HOME, content + staging, regression tests #137
8 changed files with 421 additions and 1 deletions
|
|
@ -125,7 +125,7 @@ if [ "${COLIBRI_STAGE_TEST_AGENT}" = "auto" ]; then
|
||||||
*) COLIBRI_STAGE_TEST_AGENT="YES" ;;
|
*) COLIBRI_STAGE_TEST_AGENT="YES" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
ZOT_VERSION="${ZOT_VERSION:-v0.2.42}"
|
ZOT_VERSION="${ZOT_VERSION:-v0.2.47}"
|
||||||
ZOT_REPO="${ZOT_REPO:-/home/clawdie/ai/zot}"
|
ZOT_REPO="${ZOT_REPO:-/home/clawdie/ai/zot}"
|
||||||
ZOT_ARTIFACT_DIR="${ZOT_ARTIFACT_DIR:-}"
|
ZOT_ARTIFACT_DIR="${ZOT_ARTIFACT_DIR:-}"
|
||||||
# Optional: bake the operator's DeepSeek key into the agent's auth.json (0600).
|
# Optional: bake the operator's DeepSeek key into the agent's auth.json (0600).
|
||||||
|
|
|
||||||
43
build.sh
43
build.sh
|
|
@ -2442,6 +2442,49 @@ if [ ! -f "$WORK_IMG" ]; then
|
||||||
mount_msdosfs /dev/${MD}s3 "${_seed_mount}"
|
mount_msdosfs /dev/${MD}s3 "${_seed_mount}"
|
||||||
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
|
||||||
"${_seed_mount}/README.txt"
|
"${_seed_mount}/README.txt"
|
||||||
|
|
||||||
|
# Seed agent directory with operational files.
|
||||||
|
# The seed importer (clawdie-live-seed) reads these at boot:
|
||||||
|
# env → merged into ~/.env + /usr/local/etc/colibri/provider.env
|
||||||
|
# AGENTS.md → /var/db/colibri/.local/state/zot/AGENTS.md (zot global slot)
|
||||||
|
# harness.toml → recorded for Colibri runtime.
|
||||||
|
# soul/ → staged for Hermes (dormant, loaded when Hermes is installed).
|
||||||
|
_seed_agent_dir="${_seed_mount}/clawdie"
|
||||||
|
mkdir -p "${_seed_agent_dir}"
|
||||||
|
|
||||||
|
# Operational rules for the autospawned zot agent.
|
||||||
|
if [ -f "${LIVE_SESSION_DIR}/seed/AGENTS.md" ]; then
|
||||||
|
install -m 0644 "${LIVE_SESSION_DIR}/seed/AGENTS.md" \
|
||||||
|
"${_seed_agent_dir}/AGENTS.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Harness config.
|
||||||
|
if [ -f "${LIVE_SESSION_DIR}/seed/harness.toml" ]; then
|
||||||
|
install -m 0644 "${LIVE_SESSION_DIR}/seed/harness.toml" \
|
||||||
|
"${_seed_agent_dir}/harness.toml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Provider keys: read from the build host's provider.env, write only
|
||||||
|
# the API key lines to the seed. The seed importer merges them into
|
||||||
|
# both the operator's ~/.env and the daemon's provider.env at boot.
|
||||||
|
if [ -f /usr/local/etc/colibri/provider.env ]; then
|
||||||
|
grep -E '^(DEEPSEEK_API_KEY|OPENROUTER_API_KEY)=' \
|
||||||
|
/usr/local/etc/colibri/provider.env \
|
||||||
|
> "${_seed_agent_dir}/env" 2>/dev/null || true
|
||||||
|
if [ -s "${_seed_agent_dir}/env" ]; then
|
||||||
|
chmod 0600 "${_seed_agent_dir}/env"
|
||||||
|
echo " Seeded $(wc -l < "${_seed_agent_dir}/env") API key(s) from provider.env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Layered-soul identity files (dormant — for Hermes later).
|
||||||
|
if [ -d /home/clawdie/ai/layered-soul ]; then
|
||||||
|
mkdir -p "${_seed_agent_dir}/soul"
|
||||||
|
cp -R /home/clawdie/ai/layered-soul/. "${_seed_agent_dir}/soul/" 2>/dev/null || true
|
||||||
|
rm -rf "${_seed_agent_dir}/soul/.git" 2>/dev/null || true
|
||||||
|
echo " Seeded soul backup (layered-soul)"
|
||||||
|
fi
|
||||||
|
|
||||||
sync
|
sync
|
||||||
umount "${_seed_mount}"
|
umount "${_seed_mount}"
|
||||||
rmdir "${_seed_mount}" 2>/dev/null || true
|
rmdir "${_seed_mount}" 2>/dev/null || true
|
||||||
|
|
|
||||||
49
live/operator-session/seed/AGENTS.md
Normal file
49
live/operator-session/seed/AGENTS.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Clawdie USB operator node — zot agent rules
|
||||||
|
#
|
||||||
|
# Read by zot at boot via $ZOT_HOME/AGENTS.md (global slot).
|
||||||
|
# Installed by the seed importer from the CLAWDIESEED partition.
|
||||||
|
# LLM reads this as project context in the system prompt.
|
||||||
|
|
||||||
|
## You are running on
|
||||||
|
|
||||||
|
A Clawdie USB operator node booted from a live image.
|
||||||
|
Hardware: detected at boot by clawdie-hw-probe.
|
||||||
|
Agent harness: zot (Colibri RPC mode).
|
||||||
|
Primary provider: DeepSeek (deepseek-v4-pro).
|
||||||
|
Fallback provider: OpenRouter.
|
||||||
|
|
||||||
|
## Mother
|
||||||
|
|
||||||
|
The mother node is OSA (osa.smilepowered.org), reachable via SSH as the
|
||||||
|
colibri user. The private key is on this seed at colibri/ssh/mother-mcp
|
||||||
|
and was installed to both /home/clawdie/.ssh/ and /var/db/colibri/.ssh/
|
||||||
|
at boot.
|
||||||
|
|
||||||
|
Mother runs the Colibri MCP host. Use these verbs:
|
||||||
|
- node_register: UPSERT this node's hardware profile to PostgreSQL.
|
||||||
|
- create-task: create a coordination task on the board.
|
||||||
|
- intake-task: submit a task with capability requirements.
|
||||||
|
|
||||||
|
External MCP tools are discoverable via colibri status.
|
||||||
|
|
||||||
|
## This node's capabilities
|
||||||
|
|
||||||
|
- freebsd: native FreeBSD operator and system administration.
|
||||||
|
- colibri: can query and manage the Colibri daemon.
|
||||||
|
- zot: this agent itself.
|
||||||
|
|
||||||
|
## After self-configuration
|
||||||
|
|
||||||
|
1. Verify the daemon is running: service colibri_daemon status
|
||||||
|
2. Register with mother: read CLAWDIE_HW_PROFILE from your environment
|
||||||
|
(the daemon collected it at boot) and call the node_register MCP tool
|
||||||
|
on the mother server with that JSON.
|
||||||
|
3. Install Hermes from /home/clawdie/ai/hermes-bsd per
|
||||||
|
README-FreeBSD.md for persistent agent identity.
|
||||||
|
|
||||||
|
## Colibri CLI
|
||||||
|
|
||||||
|
Commands available on this node:
|
||||||
|
- colibri status: daemon status, cost mode, scheduler, tasks.
|
||||||
|
- colibri list-tasks: coordination task board.
|
||||||
|
- colibri --help: all subcommands.
|
||||||
12
live/operator-session/seed/env.placeholder
Normal file
12
live/operator-session/seed/env.placeholder
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Provider API keys — seeded onto the CLAWDIESEED partition.
|
||||||
|
# build.sh copies this template and replaces the placeholder values
|
||||||
|
# with real keys from /usr/local/etc/colibri/provider.env on the build host.
|
||||||
|
#
|
||||||
|
# The seed importer merges these into both ~/.env (operator) and
|
||||||
|
# /usr/local/etc/colibri/provider.env (daemon), so the autospawned zot
|
||||||
|
# boots with working API keys.
|
||||||
|
#
|
||||||
|
# DO NOT commit real keys to this file. Placeholders only.
|
||||||
|
|
||||||
|
DEEPSEEK_API_KEY=SEED_DEEPSEEK_API_KEY_PLACEHOLDER
|
||||||
|
OPENROUTER_API_KEY=SEED_OPENROUTER_API_KEY_PLACEHOLDER
|
||||||
6
live/operator-session/seed/harness.toml
Normal file
6
live/operator-session/seed/harness.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Agent harness configuration — seeded onto the CLAWDIESEED partition.
|
||||||
|
# Read by colibri at boot to determine which agent binary to launch.
|
||||||
|
|
||||||
|
harness = "zot"
|
||||||
|
model = "deepseek-v4-pro"
|
||||||
|
cost_mode = "smart"
|
||||||
|
|
@ -121,6 +121,13 @@ COLIBRI_AUTOSPAWN="YES"
|
||||||
# support (DeepSeek native, OpenRouter, ~25 providers) and a built-in
|
# support (DeepSeek native, OpenRouter, ~25 providers) and a built-in
|
||||||
# Telegram bot mode. Set COLIBRI_AUTOSPAWN_BINARY=pi to switch back.
|
# Telegram bot mode. Set COLIBRI_AUTOSPAWN_BINARY=pi to switch back.
|
||||||
COLIBRI_AUTOSPAWN_BINARY="zot"
|
COLIBRI_AUTOSPAWN_BINARY="zot"
|
||||||
|
# RPC prompt sent to the autospawned zot agent on first boot.
|
||||||
|
# zot is in RPC mode and blocks on stdin; this prompt kicks it off.
|
||||||
|
# The daemon collected hw-probe at boot (CLAWDIE_HW_PROFILE); the seed
|
||||||
|
# importer installed AGENTS.md with operational context; the provider
|
||||||
|
# keys are either baked or seeded. The agent's first job: register
|
||||||
|
# this node with mother, then report its assigned capabilities.
|
||||||
|
COLIBRI_AUTOSPAWN_RPC_PROMPT="You just booted. Read CLAWDIE_HW_PROFILE from your environment and call node_register on the mother MCP server with that JSON payload. Then report what capabilities mother assigned and what tasks are available. Your AGENTS.md context explains the verbs and infrastructure."
|
||||||
# Telegram bot token — set this to enable the bot channel (@your_bot).
|
# Telegram bot token — set this to enable the bot channel (@your_bot).
|
||||||
# Leave blank to use CLI/TUI/Dashboard channels only.
|
# Leave blank to use CLI/TUI/Dashboard channels only.
|
||||||
# TELEGRAM_BOT_TOKEN=""
|
# TELEGRAM_BOT_TOKEN=""
|
||||||
|
|
|
||||||
127
tests/mcp-boundary-test.sh
Executable file
127
tests/mcp-boundary-test.sh
Executable file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/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> <msg>
|
||||||
|
|
||||||
|
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}" <<EOF
|
||||||
|
Port ${PORT}
|
||||||
|
ListenAddress 127.0.0.1
|
||||||
|
HostKey ${HK}
|
||||||
|
PidFile ${WORK}/sshd.pid
|
||||||
|
AuthorizedKeysFile ${AK}
|
||||||
|
StrictModes no
|
||||||
|
UsePAM no
|
||||||
|
PasswordAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
EOF
|
||||||
|
"${SSHDBIN}" -D -f "${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
|
||||||
176
tests/seed-import-test.sh
Executable file
176
tests/seed-import-test.sh
Executable file
|
|
@ -0,0 +1,176 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Layer 0 — clawdie-live-seed importer regression test (runs on any POSIX host).
|
||||||
|
#
|
||||||
|
# Exercises the REAL importer (live/operator-session/clawdie-live-seed) against a
|
||||||
|
# synthetic CLAWDIESEED tree, with every path redirected to a temp sandbox via the
|
||||||
|
# SEED_* override vars and CLAWDIE_SEED_TEST=1 (which skips the rc.subr handoff).
|
||||||
|
# No FreeBSD, no mount, no root required — chowns fail silently by design.
|
||||||
|
#
|
||||||
|
# It encodes the seed->runtime propagation contract as assertions:
|
||||||
|
# - operator home: .env (app keys), vault-bootstrap.env (BW_*), ssh material
|
||||||
|
# - daemon home: outbound ssh material (mother-mcp key), NO authorized_keys
|
||||||
|
# - provider.env: app provider keys only (no BW_*)
|
||||||
|
# - staging: soul/ tree, harness.toml, agent-name, active-agent, raw env
|
||||||
|
# - idempotency: re-import does not duplicate keys / known_hosts
|
||||||
|
#
|
||||||
|
# PENDING group: AGENTS.md -> zot home. This is the change Hermes is pushing.
|
||||||
|
# It is xfail (informational) until the importer learns to install AGENTS.md;
|
||||||
|
# re-run with REQUIRE_AGENTS_MD=1 after the patch to enforce it as required.
|
||||||
|
#
|
||||||
|
# Usage: sh tests/seed-import-test.sh # current contract must pass
|
||||||
|
# REQUIRE_AGENTS_MD=1 sh tests/... # also enforce the AGENTS.md patch
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
IMPORTER="${IMPORTER:-${SCRIPT_DIR}/../live/operator-session/clawdie-live-seed}"
|
||||||
|
|
||||||
|
if [ ! -r "${IMPORTER}" ]; then
|
||||||
|
echo "FATAL: importer not found at ${IMPORTER}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK=$(mktemp -d "${TMPDIR:-/tmp}/seed-import-test.XXXXXX") || exit 2
|
||||||
|
trap 'rm -rf "${WORK}"' EXIT INT TERM
|
||||||
|
|
||||||
|
# --- override every path the importer touches into the sandbox ---------------
|
||||||
|
TEST_USER=$(id -un)
|
||||||
|
export CLAWDIE_SEED_TEST=1
|
||||||
|
export SEED_MOUNT="${WORK}/seed"
|
||||||
|
export SEED_LOG="${WORK}/seed.log"
|
||||||
|
export SEED_USER="${TEST_USER}"
|
||||||
|
export SEED_USER_HOME="${WORK}/operator-home"
|
||||||
|
export SEED_DAEMON_USER="${TEST_USER}"
|
||||||
|
export SEED_DAEMON_HOME="${WORK}/daemon-home"
|
||||||
|
export SEED_IMPORT_ROOT="${WORK}/import-root"
|
||||||
|
export SEED_PROVIDER_ENV="${WORK}/provider.env"
|
||||||
|
# Contract under test for the AGENTS.md patch: importer installs it to zot's
|
||||||
|
# global slot under the daemon home. Keep this in one place so it tracks the
|
||||||
|
# final decision (ZOT_HOME pin) without edits scattered through the test.
|
||||||
|
ZOT_HOME_REL=".local/state/zot"
|
||||||
|
EXPECT_AGENTS_MD="${SEED_DAEMON_HOME}/${ZOT_HOME_REL}/AGENTS.md"
|
||||||
|
|
||||||
|
mkdir -p "${SEED_USER_HOME}" "${SEED_DAEMON_HOME}" "${SEED_IMPORT_ROOT}"
|
||||||
|
|
||||||
|
# --- build a synthetic single-agent seed (the live-USB case) ------------------
|
||||||
|
SEED_AGENT="${SEED_MOUNT}/clawdie"
|
||||||
|
mkdir -p "${SEED_AGENT}/ssh" "${SEED_AGENT}/soul/memories"
|
||||||
|
|
||||||
|
cat >"${SEED_AGENT}/env" <<'EOF'
|
||||||
|
# operator-provided secrets (plaintext FAT32 by design)
|
||||||
|
DEEPSEEK_API_KEY=sk-test-deepseek
|
||||||
|
DEEPSEEK_MODEL=deepseek-v4
|
||||||
|
BW_CLIENTID=user.test-client
|
||||||
|
BW_CLIENTSECRET=test-secret
|
||||||
|
BW_PASSWORD=test-master-pw
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >"${SEED_AGENT}/harness.toml" <<'EOF'
|
||||||
|
harness = "zot"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >"${SEED_AGENT}/AGENTS.md" <<'EOF'
|
||||||
|
# Operational rules (zot reads this as project context)
|
||||||
|
- mother is OSA, reachable via the mother-mcp key on this seed
|
||||||
|
- verbs: node_register, create-task, intake-task
|
||||||
|
- this node is a USB operator, capability: freebsd
|
||||||
|
- install Hermes from /home/clawdie/ai/hermes-bsd
|
||||||
|
CANARY_AGENTS_MARKER=zot-sees-this
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# soul tree (dormant; must be STAGED, not activated, in 0.12)
|
||||||
|
cat >"${SEED_AGENT}/soul/SOUL.md" <<'EOF'
|
||||||
|
# SOUL (staged for Hermes, dormant)
|
||||||
|
EOF
|
||||||
|
cat >"${SEED_AGENT}/soul/memories/USER.md" <<'EOF'
|
||||||
|
# USER (staged)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ssh material: inbound authorized_keys + outbound client (config, key, known_hosts)
|
||||||
|
echo "ssh-ed25519 AAAAINBOUNDtestkey operator@laptop" >"${SEED_AGENT}/ssh/authorized_keys"
|
||||||
|
cat >"${SEED_AGENT}/ssh/config" <<'EOF'
|
||||||
|
Host mother
|
||||||
|
HostName 100.72.229.63
|
||||||
|
User colibri
|
||||||
|
IdentityFile ~/.ssh/mother-mcp
|
||||||
|
EOF
|
||||||
|
echo "-----BEGIN OPENSSH PRIVATE KEY-----TEST-----END-----" >"${SEED_AGENT}/ssh/mother-mcp"
|
||||||
|
echo "ssh-ed25519 AAAAMOTHERPUBkey colibri@mother" >"${SEED_AGENT}/ssh/mother-mcp.pub"
|
||||||
|
echo "mother-host ssh-ed25519 AAAAMOTHERhostkey" >"${SEED_AGENT}/ssh/known_hosts"
|
||||||
|
|
||||||
|
# --- assertion harness -------------------------------------------------------
|
||||||
|
PASS=0
|
||||||
|
REQ_FAIL=0
|
||||||
|
PEND_FAIL=0
|
||||||
|
|
||||||
|
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
|
||||||
|
fail() { REQ_FAIL=$((REQ_FAIL+1)); printf ' FAIL %s\n' "$1"; }
|
||||||
|
pend() { PEND_FAIL=$((PEND_FAIL+1)); printf ' PEND %s\n' "$1"; }
|
||||||
|
|
||||||
|
# fail() vs pend() chosen by tag arg ($1 = required|pending)
|
||||||
|
report() { # report <required|pending> <passed 0|1> <msg>
|
||||||
|
if [ "$2" -eq 1 ]; then ok "$3"
|
||||||
|
elif [ "$1" = pending ] && [ -z "${REQUIRE_AGENTS_MD:-}" ]; then pend "$3"
|
||||||
|
else fail "$3"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
exists() { [ -e "$2" ] && report "$1" 1 "$3" || report "$1" 0 "$3"; }
|
||||||
|
not_exists() { [ ! -e "$2" ] && report "$1" 1 "$3" || report "$1" 0 "$3"; }
|
||||||
|
contains() { [ -f "$2" ] && grep -q "$3" "$2" 2>/dev/null && report "$1" 1 "$4" || report "$1" 0 "$4"; }
|
||||||
|
absent_in() { { [ ! -f "$2" ] || ! grep -q "$3" "$2" 2>/dev/null; } && report "$1" 1 "$4" || report "$1" 0 "$4"; }
|
||||||
|
count_is() { _c=$(grep -c "$3" "$2" 2>/dev/null || echo 0); [ "$_c" = "$4" ] && report "$1" 1 "$5 (got $_c)" || report "$1" 0 "$5 (got $_c)"; }
|
||||||
|
|
||||||
|
# --- run the real importer ---------------------------------------------------
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${IMPORTER}"
|
||||||
|
echo "== import pass 1 =="
|
||||||
|
_seed_import_tree
|
||||||
|
|
||||||
|
OP_HOME="${SEED_USER_HOME}"
|
||||||
|
DM_HOME="${SEED_DAEMON_HOME}"
|
||||||
|
STAGE="${SEED_IMPORT_ROOT}/clawdie"
|
||||||
|
|
||||||
|
echo "-- operator home --"
|
||||||
|
contains required "${OP_HOME}/.env" '^DEEPSEEK_API_KEY=sk-test-deepseek' "DEEPSEEK key in operator ~/.env"
|
||||||
|
absent_in required "${OP_HOME}/.env" '^BW_PASSWORD=' "BW_* NOT in operator ~/.env"
|
||||||
|
contains required "${OP_HOME}/.config/vault-bootstrap.env" '^BW_PASSWORD=test-master-pw' "BW_PASSWORD in vault-bootstrap.env"
|
||||||
|
exists required "${OP_HOME}/.ssh/authorized_keys" "operator authorized_keys installed"
|
||||||
|
exists required "${OP_HOME}/.ssh/config" "operator ssh config installed"
|
||||||
|
exists required "${OP_HOME}/.ssh/mother-mcp" "operator mother-mcp private key installed"
|
||||||
|
contains required "${OP_HOME}/.ssh/known_hosts" 'mother-host' "operator known_hosts has mother host key"
|
||||||
|
|
||||||
|
echo "-- daemon home (colibri spawns the outbound MCP SSH) --"
|
||||||
|
exists required "${DM_HOME}/.ssh/mother-mcp" "daemon mother-mcp private key installed"
|
||||||
|
exists required "${DM_HOME}/.ssh/config" "daemon ssh config installed"
|
||||||
|
not_exists required "${DM_HOME}/.ssh/authorized_keys" "daemon has NO authorized_keys (inbound is operator-only)"
|
||||||
|
|
||||||
|
echo "-- provider.env (daemon autospawn keys; root-owned) --"
|
||||||
|
contains required "${SEED_PROVIDER_ENV}" '^DEEPSEEK_API_KEY=sk-test-deepseek' "DEEPSEEK key in provider.env"
|
||||||
|
absent_in required "${SEED_PROVIDER_ENV}" '^BW_PASSWORD=' "BW_* NOT in provider.env"
|
||||||
|
|
||||||
|
echo "-- staging (dormant payload) --"
|
||||||
|
exists required "${STAGE}/soul/SOUL.md" "soul/ tree staged"
|
||||||
|
exists required "${STAGE}/soul/memories/USER.md" "soul memories staged"
|
||||||
|
contains required "${STAGE}/harness.toml" 'harness = "zot"' "harness.toml recorded"
|
||||||
|
contains required "${STAGE}/agent-name" '^clawdie$' "agent-name staged"
|
||||||
|
contains required "${SEED_IMPORT_ROOT}/active-agent" '^clawdie$' "active-agent recorded"
|
||||||
|
exists required "${STAGE}/env" "raw env staged (0600)"
|
||||||
|
|
||||||
|
echo "-- PENDING: AGENTS.md -> zot global slot (Hermes's importer patch) --"
|
||||||
|
exists pending "${EXPECT_AGENTS_MD}" "AGENTS.md installed to \$ZOT_HOME"
|
||||||
|
contains pending "${EXPECT_AGENTS_MD}" 'CANARY_AGENTS_MARKER=zot-sees-this' "AGENTS.md content intact at zot slot"
|
||||||
|
|
||||||
|
# --- idempotency: a second import must not duplicate -------------------------
|
||||||
|
echo "== import pass 2 (idempotency) =="
|
||||||
|
_seed_import_tree
|
||||||
|
count_is required "${OP_HOME}/.env" '^DEEPSEEK_API_KEY=' 1 "exactly one DEEPSEEK key in ~/.env after re-import"
|
||||||
|
count_is required "${SEED_PROVIDER_ENV}" '^DEEPSEEK_API_KEY=' 1 "exactly one DEEPSEEK key in provider.env after re-import"
|
||||||
|
count_is required "${OP_HOME}/.ssh/known_hosts" 'mother-host' 1 "known_hosts not duplicated after re-import"
|
||||||
|
|
||||||
|
# --- summary -----------------------------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "RESULT: ${PASS} passed, ${REQ_FAIL} required-fail, ${PEND_FAIL} pending"
|
||||||
|
if [ -n "${REQUIRE_AGENTS_MD:-}" ] && [ "${PEND_FAIL}" -gt 0 ]; then
|
||||||
|
echo " (REQUIRE_AGENTS_MD set: pending failures count as required)"
|
||||||
|
fi
|
||||||
|
[ "${REQ_FAIL}" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
Loading…
Add table
Reference in a new issue