#!/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" # 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"