diff --git a/build.cfg b/build.cfg index 7c4a0eb..db29694 100644 --- a/build.cfg +++ b/build.cfg @@ -125,7 +125,7 @@ if [ "${COLIBRI_STAGE_TEST_AGENT}" = "auto" ]; then *) COLIBRI_STAGE_TEST_AGENT="YES" ;; esac fi -ZOT_VERSION="${ZOT_VERSION:-v0.2.42}" +ZOT_VERSION="${ZOT_VERSION:-v0.2.47}" ZOT_REPO="${ZOT_REPO:-/home/clawdie/ai/zot}" ZOT_ARTIFACT_DIR="${ZOT_ARTIFACT_DIR:-}" # Optional: bake the operator's DeepSeek key into the agent's auth.json (0600). diff --git a/build.sh b/build.sh index afd5bd6..e52db33 100755 --- a/build.sh +++ b/build.sh @@ -2442,6 +2442,49 @@ if [ ! -f "$WORK_IMG" ]; then mount_msdosfs /dev/${MD}s3 "${_seed_mount}" install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.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 umount "${_seed_mount}" rmdir "${_seed_mount}" 2>/dev/null || true diff --git a/live/operator-session/seed/AGENTS.md b/live/operator-session/seed/AGENTS.md new file mode 100644 index 0000000..70af139 --- /dev/null +++ b/live/operator-session/seed/AGENTS.md @@ -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. diff --git a/live/operator-session/seed/env.placeholder b/live/operator-session/seed/env.placeholder new file mode 100644 index 0000000..579ec36 --- /dev/null +++ b/live/operator-session/seed/env.placeholder @@ -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 diff --git a/live/operator-session/seed/harness.toml b/live/operator-session/seed/harness.toml new file mode 100644 index 0000000..3cef5c1 --- /dev/null +++ b/live/operator-session/seed/harness.toml @@ -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" diff --git a/scripts/stage-colibri-iso.sh b/scripts/stage-colibri-iso.sh index 10b73e3..22346e1 100755 --- a/scripts/stage-colibri-iso.sh +++ b/scripts/stage-colibri-iso.sh @@ -121,6 +121,13 @@ COLIBRI_AUTOSPAWN="YES" # support (DeepSeek native, OpenRouter, ~25 providers) and a built-in # Telegram bot mode. Set COLIBRI_AUTOSPAWN_BINARY=pi to switch back. 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). # Leave blank to use CLI/TUI/Dashboard channels only. # TELEGRAM_BOT_TOKEN="" diff --git a/tests/mcp-boundary-test.sh b/tests/mcp-boundary-test.sh new file mode 100755 index 0000000..4ee15a1 --- /dev/null +++ b/tests/mcp-boundary-test.sh @@ -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> + +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 diff --git a/tests/seed-import-test.sh b/tests/seed-import-test.sh new file mode 100755 index 0000000..07f08bc --- /dev/null +++ b/tests/seed-import-test.sh @@ -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 + 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