From add1cea732cdf5c0415ded5ddb4ef40d00daf4fd Mon Sep 17 00:00:00 2001 From: Matteo Cherubini Date: Sat, 20 Jun 2026 22:23:57 +0200 Subject: [PATCH] feat: Implement 'ensure-genome-vault' management script --- deploy/nexus/ensure-genome-vault.sh | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 deploy/nexus/ensure-genome-vault.sh diff --git a/deploy/nexus/ensure-genome-vault.sh b/deploy/nexus/ensure-genome-vault.sh new file mode 100644 index 0000000..42ed105 --- /dev/null +++ b/deploy/nexus/ensure-genome-vault.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# ensure-genome-vault [--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 [--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" + +# ── 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"