knowledge-genome-orchestrator/deploy/nexus/ensure-genome-vault.sh

126 lines
5.2 KiB
Bash

#!/bin/bash
# ensure-genome-vault <genome> [--status-only]
#
# Idempotent, unified command for managing genome vaults.
# Called by n8n during genome creation and as a safety net mechanism.
#
# Operation workflow:
# - Vault absent -> Clone from Forgejo (loopback) + track develop branch
# - Vault present -> Realign to origin/develop (treated as a rebuildable scratchpad)
# - Post-clone/fetch -> Write raw/.stignore and register/update the Syncthing folder.
#
# Source of truth is Forgejo. Vaults are scratch spaces and not backed up directly.
# All operations run locally via loopback.
set -euo pipefail
genome="${1:?usage: ensure-genome-vault <genome> [--status-only]}"
mode="${2:-}"
# Slug validation inside the script to prevent path/URL traversal:
# Lowercase kebab-case, no '/', '..', or spaces.
[[ "$genome" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]] || { echo '{"status":"error","reason":"invalid genome name"}'; exit 1; }
set -a; . "${HOME}/.config/knowledge-genome.env"; set +a
: "${GENOME_VAULTS_ROOT:=/srv/genome-vaults}"
: "${GENOME_BASE:=develop}"
: "${FORGEJO_USER:=n8n-bot}"
: "${FORGEJO_HOST:=127.0.0.1:3001}"
: "${FORGEJO_OWNER:=Keru}"
: "${SYNCTHING_URL:=http://127.0.0.1:8384}"
vault="${GENOME_VAULTS_ROOT}/${genome}"
fid="${genome}-public"
clone_url="http://${FORGEJO_USER}@${FORGEJO_HOST}/${FORGEJO_OWNER}/${genome}.git"
export GIT_ASKPASS=/usr/local/bin/genome-askpass # Provides the n8n-bot token
mkdir -p "$GENOME_VAULTS_ROOT"
# ── 1. Clone (if missing) or realign (if present) ────────────────────────────
if [[ ! -d "${vault}/.git" ]]; then
[[ "$mode" == "--status-only" ]] && { printf '{"status":"absent","genome":"%s"}\n' "$genome"; exit 0; }
git clone -q "$clone_url" "$vault"
cd "$vault"
if git show-ref --verify --quiet "refs/remotes/origin/${GENOME_BASE}"; then
git switch -q -c "$GENOME_BASE" --track "origin/${GENOME_BASE}" 2>/dev/null || git switch -q "$GENOME_BASE"
else
# develop does not exist on remote yet: create it from current base and publish
git switch -q -c "$GENOME_BASE"
git push -q "$clone_url" "${GENOME_BASE}:${GENOME_BASE}"
fi
state="cloned"
else
cd "$vault"
if [[ "$mode" == "--status-only" ]]; then
printf '{"status":"present","genome":"%s","head":"%s"}\n' "$genome" "$(git rev-parse --short HEAD)"
exit 0
fi
git fetch -q origin
if git show-ref --verify --quiet "refs/remotes/origin/${GENOME_BASE}"; then
git switch -q "$GENOME_BASE" 2>/dev/null || git switch -q -c "$GENOME_BASE" --track "origin/${GENOME_BASE}"
# GUARD: hard reset is allowed ONLY if the working tree is clean.
# If Syncthing has already written uncommitted raw files, DO NOT destroy them: soft fast-forward.
if [[ -z "$(git status --porcelain -- raw/ 2>/dev/null)" ]]; then
git reset -q --hard "origin/${GENOME_BASE}"
state="realigned"
else
git merge -q --ff-only "origin/${GENOME_BASE}" 2>/dev/null || true
state="realigned-kept-dirty"
fi
else
git switch -q -c "$GENOME_BASE" 2>/dev/null || true
git push -q "$clone_url" "${GENOME_BASE}:${GENOME_BASE}"
state="base-created"
fi
fi
# ── 2. raw/.stignore + exclusion from git (infrastructure, not content) ────────────
mkdir -p "${vault}/raw"
cat > "${vault}/raw/.stignore" <<'EOF'
// Knowledge Genome — Syncthing exclusions for raw/
// NEVER unencrypted private data: git-crypt protects INSIDE the repo, not in Syncthing transit
private
// Obsidian / editor noise
.obsidian
.trash
*.tmp
workspace*.json
// security
.git
EOF
# .stignore must not be included in genome commits
grep -qxF 'raw/.stignore' "${vault}/.git/info/exclude" 2>/dev/null \
|| echo 'raw/.stignore' >> "${vault}/.git/info/exclude"
# Syncthing folder marker: must exist on disk (locally, NOT on Git).
# Without it, Syncthing refuses to scan (“folder marker missing”).
mkdir -p "${vault}/raw/.stfolder"
# .stfolder must not be included in genome commits
grep -qxF 'raw/.stfolder' "${vault}/.git/info/exclude" 2>/dev/null \
|| echo 'raw/.stfolder' >> "${vault}/.git/info/exclude"
# ── 3. Idempotent Syncthing folder configuration (best-effort, does not block the vault) ────────
folder_state="skipped(no api key)"
if [[ -n "${SYNCTHING_API_KEY:-}" ]]; then
if curl -fsS -o /dev/null -H "X-API-Key: ${SYNCTHING_API_KEY}" \
"${SYNCTHING_URL}/rest/config/folders/${fid}" 2>/dev/null; then
folder_state="exists"
else
body="$(curl -fsS -H "X-API-Key: ${SYNCTHING_API_KEY}" \
"${SYNCTHING_URL}/rest/config/defaults/folder" \
| jq --arg id "$fid" --arg label "${genome} (raw public)" --arg path "${vault}/raw" \
'.id=$id | .label=$label | .path=$path | .type="sendreceive"
| .fsWatcherEnabled=true | .rescanIntervalS=3600')"
if curl -fsS -o /dev/null -X PUT \
-H "X-API-Key: ${SYNCTHING_API_KEY}" -H "Content-Type: application/json" \
-d "$body" "${SYNCTHING_URL}/rest/config/folders/${fid}" 2>/dev/null; then
folder_state="created"
else
folder_state="error(check syncthing api)"
fi
fi
fi
printf '{"status":"ok","genome":"%s","vault":"%s","state":"%s","syncthing_folder":"%s"}\n' \
"$genome" "$vault" "$state" "$folder_state"