commit eac77ef7b4b5a87f0a35a1181dacb4013b09b996 Author: Vault Sovereign Date: Sat Dec 27 00:25:00 2025 +0000 Initial commit: VaultMesh Skills collection Collection of operational skills for VaultMesh infrastructure including: - backup-sovereign: Backup and recovery operations - btc-anchor: Bitcoin anchoring - cloudflare-tunnel-manager: Cloudflare tunnel management - container-registry: Container registry operations - disaster-recovery: Disaster recovery procedures - dns-sovereign: DNS management - eth-anchor: Ethereum anchoring - gitea-bootstrap: Gitea setup and configuration - hetzner-bootstrap: Hetzner server provisioning - merkle-forest: Merkle tree operations - node-hardening: Node security hardening - operator-bootstrap: Operator initialization - proof-verifier: Cryptographic proof verification - rfc3161-anchor: RFC3161 timestamping - secrets-vault: Secrets management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 diff --git a/backup-sovereign/SKILL.md b/backup-sovereign/SKILL.md new file mode 100644 index 0000000..3c45842 --- /dev/null +++ b/backup-sovereign/SKILL.md @@ -0,0 +1,177 @@ +--- +name: backup-sovereign +description: > + Create encrypted, verifiable backups with proof receipts (BLAKE3 + ROOT.txt) + and mandatory restore drill. Uses age encryption for modern, simple UX. + Designed for sovereign EU infrastructure. Use after node-hardening completes. + Triggers: 'backup node', 'encrypted backup', 'create backup', 'restore drill', + 'generate proof receipts', 'verify backup', 'backup with proof'. +version: 1.0.0 +--- + +# Backup Sovereign + +High-risk Tier 1 skill for creating encrypted, verifiable backups. All backups include BLAKE3 proof receipts and require a mandatory restore drill to verify recoverability. + +## Quick Start + +```bash +# Set required parameters +export BACKUP_SOURCES="$HOME/infrastructure,$HOME/.claude/skills" +export AGE_RECIPIENT_FILE="$HOME/.config/age/recipients.txt" +export AGE_IDENTITY_FILE="$HOME/.config/age/identity.txt" + +# Optional: customize +export NODE_NAME="node-a" +export BACKUP_LABEL="daily" + +# Run preflight +./scripts/00_preflight.sh + +# Plan phases (safe to run, shows what WILL happen) +./scripts/10_backup_plan.sh +./scripts/20_encrypt_plan.sh + +# Apply phases (REQUIRES DRY_RUN=0 and confirmation) +export DRY_RUN=0 +./scripts/11_backup_apply.sh # Type confirmation phrase +./scripts/21_encrypt_apply.sh # Type confirmation phrase + +# Generate proof receipts +./scripts/30_generate_proof.sh + +# Verify artifacts +./scripts/40_verify_backup.sh + +# MANDATORY: Restore drill +./scripts/50_restore_drill.sh # Type confirmation phrase + +# Status and report +./scripts/90_verify.sh +./scripts/99_report.sh +``` + +## Workflow + +### Phase 0: Preflight (00) +Check dependencies: tar, gzip, age, b3sum. +Verify BACKUP_SOURCES paths exist. +Check available disk space. + +### Phase 1: Backup (10-11) +**Two-phase operation with DRY_RUN gate.** + +Plan phase shows: +- Source paths to archive +- Exclude patterns +- Output directory and run ID +- Estimated archive size + +Apply phase executes: +- Creates tar.gz archive +- Generates manifest.json with BLAKE3 hashes +- Records excludes.txt + +### Phase 2: Encrypt (20-21) +**Two-phase operation with DRY_RUN gate.** + +Plan phase shows: +- Encryption method (age) +- Recipient file location +- Output file path + +Apply phase executes: +- Encrypts archive with age +- Creates archive.tar.gz.age + +### Phase 3: Proof (30) +Generate cryptographic proof receipts: +- BLAKE3 hash of manifest.json +- BLAKE3 hash of encrypted archive +- ROOT.txt (composite hash for anchoring) +- PROOF.json (metadata receipt) + +### Phase 4: Verify (40) +Verify all artifacts exist and ROOT.txt is valid. + +### Phase 5: Restore Drill (50) **MANDATORY** +**DRY_RUN gate + CONFIRM_PHRASE** + +This phase is required to validate backup recoverability: +- Decrypts archive to temp directory +- Extracts and verifies file count +- Records restore location + +### Phase 6: Status + Report (90-99) +Generate JSON status matrix and markdown audit report. + +## Inputs + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| BACKUP_SOURCES | Yes | - | Comma-separated paths to backup | +| AGE_RECIPIENT_FILE | Yes | - | File with age public key(s) | +| AGE_IDENTITY_FILE | Yes | - | File with age private key (for restore) | +| NODE_NAME | No | node-a | Node identifier | +| BACKUP_LABEL | No | manual | Label for this backup run | +| BACKUP_EXCLUDES | No | .git,node_modules,target,dist,outputs | Exclude patterns | +| OUTPUT_DIR | No | outputs | Output directory | +| DRY_RUN | No | 1 | Set to 0 to enable apply scripts | +| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase | +| CONFIRM_PHRASE | No | I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS | Safety phrase | + +## Outputs + +| File | Description | +|------|-------------| +| `outputs/runs//archive.tar.gz` | Unencrypted archive | +| `outputs/runs//archive.tar.gz.age` | Encrypted archive | +| `outputs/runs//manifest.json` | File list + sizes + BLAKE3 hashes | +| `outputs/runs//ROOT.txt` | BLAKE3 root (for anchoring) | +| `outputs/runs//PROOF.json` | Metadata receipt | +| `outputs/runs//excludes.txt` | Exclude patterns used | +| `outputs/status_matrix.json` | Verification results | +| `outputs/audit_report.md` | Human-readable audit trail | + +## Safety Guarantees + +1. **DRY_RUN=1 by default** - Apply scripts refuse to run without explicit DRY_RUN=0 +2. **CONFIRM_PHRASE required** - Must type exact phrase to proceed +3. **Mandatory restore drill** - Untested backups are not trusted +4. **BLAKE3 hashes** - Cryptographic integrity verification +5. **ROOT.txt for anchoring** - Can be submitted to merkle-forest/rfc3161-anchor +6. **Per-run isolation** - Each backup is immutable once created +7. **All scripts idempotent** - Safe to run multiple times + +## age Key Setup + +If you don't have age keys yet: + +```bash +# Generate identity (private key) +age-keygen -o ~/.config/age/identity.txt + +# Extract public key to recipients file +age-keygen -y ~/.config/age/identity.txt > ~/.config/age/recipients.txt +``` + +## EU Compliance + +| Aspect | Value | +|--------|-------| +| Data Residency | EU (Ireland - Dublin) | +| GDPR Applicable | Yes (depends on backup content) | +| Jurisdiction | Irish Law | +| Encryption at Rest | Yes (age) | + +## References + +- [Recovery Notes](references/recovery_notes.md) + +## Next Steps + +After completing backup-sovereign: +1. Store encrypted bundle off-node (secondary disk / object store) +2. Test restore on a different machine (recommended) +3. Optionally anchor ROOT.txt with rfc3161-anchor skill +4. Proceed to **disaster-recovery** skill diff --git a/backup-sovereign/checks/check_restore.sh b/backup-sovereign/checks/check_restore.sh new file mode 100755 index 0000000..7094234 --- /dev/null +++ b/backup-sovereign/checks/check_restore.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Check: Last backup has passed restore drill +# Returns 0 if restore drill completed, 1 otherwise + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" + +# Check for last run pointer +[[ -f "$OUTPUT_DIR/last_run_dir.txt" ]] || exit 1 + +run_dir="$(cat "$OUTPUT_DIR/last_run_dir.txt")" + +# Check for restore drill completion +[[ -f "$run_dir/last_restore_dir.txt" ]] diff --git a/backup-sovereign/checks/check_space.sh b/backup-sovereign/checks/check_space.sh new file mode 100755 index 0000000..41873e5 --- /dev/null +++ b/backup-sovereign/checks/check_space.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Check: Sufficient disk space available +# Returns 0 if >100MB available, 1 otherwise + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" + +# Ensure output dir exists for df check +mkdir -p "$OUTPUT_DIR" + +# Get available space in KB +avail=$(df -P "$OUTPUT_DIR" | awk 'NR==2 {print $4}') + +# Require at least 100MB (102400 KB) +[[ "$avail" -ge 102400 ]] diff --git a/backup-sovereign/checks/check_tools.sh b/backup-sovereign/checks/check_tools.sh new file mode 100755 index 0000000..8ef64da --- /dev/null +++ b/backup-sovereign/checks/check_tools.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Check: Required tools are installed +# Returns 0 if all tools present, 1 otherwise + +set -euo pipefail + +command -v tar &>/dev/null && \ +command -v gzip &>/dev/null && \ +command -v age &>/dev/null && \ +(command -v b3sum &>/dev/null || command -v blake3 &>/dev/null) diff --git a/backup-sovereign/config.json b/backup-sovereign/config.json new file mode 100644 index 0000000..0a08b6f --- /dev/null +++ b/backup-sovereign/config.json @@ -0,0 +1,50 @@ +{ + "version": "1.0.0", + "skill": "backup-sovereign", + "description": "Encrypted, verifiable backups with BLAKE3 receipts + mandatory restore drill", + "parameters": { + "required": [ + "BACKUP_SOURCES", + "AGE_RECIPIENT_FILE", + "AGE_IDENTITY_FILE" + ], + "optional": { + "NODE_NAME": "node-a", + "BACKUP_LABEL": "manual", + "BACKUP_EXCLUDES": ".git,node_modules,target,dist,outputs", + "OUTPUT_DIR": "outputs", + "DRY_RUN": 1, + "REQUIRE_CONFIRM": 1, + "CONFIRM_PHRASE": "I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS" + } + }, + "phases": { + "preflight": ["00_preflight.sh"], + "backup": { + "plan": ["10_backup_plan.sh"], + "apply": ["11_backup_apply.sh"] + }, + "encrypt": { + "plan": ["20_encrypt_plan.sh"], + "apply": ["21_encrypt_apply.sh"] + }, + "proof": ["30_generate_proof.sh"], + "verify": ["40_verify_backup.sh", "50_restore_drill.sh"], + "status": ["90_verify.sh"], + "report": ["99_report.sh"] + }, + "checks": { + "tools": ["check_tools.sh"], + "space": ["check_space.sh"], + "restore": ["check_restore.sh"] + }, + "rollback_order": [ + "undo_last_backup.sh", + "purge_outputs.sh" + ], + "eu_compliance": { + "data_residency": "EU", + "jurisdiction": "Ireland", + "gdpr_applicable": true + } +} diff --git a/backup-sovereign/references/recovery_notes.md b/backup-sovereign/references/recovery_notes.md new file mode 100644 index 0000000..cbfb910 --- /dev/null +++ b/backup-sovereign/references/recovery_notes.md @@ -0,0 +1,135 @@ +# Recovery Notes + +## Overview + +This document describes recovery procedures for backup-sovereign backups. + +## Prerequisites + +- `age` installed (for decryption) +- Access to AGE_IDENTITY_FILE (private key) +- Sufficient disk space for extraction + +## Standard Recovery + +### 1. Locate Backup + +Find your encrypted backup: +```bash +ls ~/.claude/skills/backup-sovereign/outputs/runs/ +``` + +### 2. Decrypt Archive + +```bash +# Set identity file +export AGE_IDENTITY_FILE="$HOME/.config/age/identity.txt" + +# Decrypt +age -d -i "$AGE_IDENTITY_FILE" \ + -o archive.tar.gz \ + archive.tar.gz.age +``` + +### 3. Extract + +```bash +# Extract to current directory +tar -xzf archive.tar.gz + +# Or extract to specific location +tar -xzf archive.tar.gz -C /path/to/restore/ +``` + +### 4. Verify Integrity + +Compare BLAKE3 hash with manifest: +```bash +# Compute hash of archive +b3sum archive.tar.gz + +# Compare with value in manifest.json +cat manifest.json | grep blake3 +``` + +## Disaster Recovery + +If you've lost access to your primary system: + +1. **Obtain encrypted backup** from off-site storage +2. **Obtain identity file** from secure backup location +3. Follow standard recovery steps above + +## Verify ROOT + +To verify the backup hasn't been tampered with: + +```bash +# Compute manifest hash +MANIFEST_B3=$(b3sum manifest.json | awk '{print $1}') + +# Compute encrypted archive hash +ENC_B3=$(b3sum archive.tar.gz.age | awk '{print $1}') + +# Compute ROOT +echo -n "${MANIFEST_B3}${ENC_B3}" | b3sum + +# Compare with ROOT.txt +cat ROOT.txt +``` + +## Key Management + +### age Keys + +- **Identity file** (private key): Keep secure, backed up separately +- **Recipients file** (public key): Can be shared, used for encryption + +### Generate New Keys + +If you need new keys: +```bash +# Generate identity +age-keygen -o ~/.config/age/identity.txt + +# Extract public key +age-keygen -y ~/.config/age/identity.txt > ~/.config/age/recipients.txt +``` + +### Key Rotation + +1. Generate new keypair +2. Add new public key to recipients file +3. Keep old identity file for decrypting old backups +4. New backups will be encrypted to all recipients + +## Troubleshooting + +### "age: error: no identity matched any of the recipients" + +- Wrong identity file +- Backup was encrypted with different key +- Solution: Use correct identity file + +### "tar: Error opening archive" + +- Corrupted archive +- Incomplete download +- Solution: Verify BLAKE3 hash, re-download if needed + +### "b3sum: command not found" + +- Install b3sum: `cargo install b3sum` or use package manager +- Alternative: Use `blake3` CLI if available + +## Security Considerations + +1. **Never store identity file with encrypted backups** +2. **Use passphrase-protected identity** for extra security +3. **Test restore drill regularly** - backups that haven't been tested aren't backups +4. **Store backups off-site** - same location defeats the purpose + +## References + +- [age encryption](https://age-encryption.org/) +- [BLAKE3 hash](https://github.com/BLAKE3-team/BLAKE3) diff --git a/backup-sovereign/scripts/00_preflight.sh b/backup-sovereign/scripts/00_preflight.sh new file mode 100755 index 0000000..7f7c463 --- /dev/null +++ b/backup-sovereign/scripts/00_preflight.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${BACKUP_SOURCES:=}" +: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +check_tool() { + local tool="$1" + if command -v "$tool" &>/dev/null; then + log_info "Found: $tool ($(command -v "$tool"))" + return 0 + else + log_warn "Missing: $tool" + return 1 + fi +} + +check_b3sum() { + if command -v b3sum &>/dev/null; then + log_info "Found: b3sum ($(command -v b3sum))" + return 0 + elif command -v blake3 &>/dev/null; then + log_info "Found: blake3 ($(command -v blake3))" + return 0 + else + log_warn "Missing: b3sum or blake3" + return 1 + fi +} + +main() { + log_info "Starting $SCRIPT_NAME..." + mkdir -p "$OUTPUT_DIR" + + local missing=0 + + log_info "=== Required Tools ===" + check_tool tar || ((missing++)) + check_tool gzip || ((missing++)) + check_tool age || ((missing++)) + check_b3sum || ((missing++)) + check_tool stat || ((missing++)) + check_tool find || ((missing++)) + + log_info "=== Backup Sources ===" + if [[ -z "$BACKUP_SOURCES" ]]; then + log_warn "BACKUP_SOURCES not set (required for backup)" + else + IFS=',' read -r -a sources <<< "$BACKUP_SOURCES" + for src in "${sources[@]}"; do + # Expand ~ if present + src="${src/#\~/$HOME}" + if [[ -e "$src" ]]; then + log_info "Source exists: $src" + else + log_warn "Source missing: $src" + fi + done + fi + + log_info "=== Encryption Files ===" + if [[ -n "${AGE_RECIPIENT_FILE:-}" ]]; then + if [[ -f "$AGE_RECIPIENT_FILE" ]]; then + log_info "AGE_RECIPIENT_FILE exists: $AGE_RECIPIENT_FILE" + else + log_warn "AGE_RECIPIENT_FILE missing: $AGE_RECIPIENT_FILE" + fi + else + log_warn "AGE_RECIPIENT_FILE not set (required for encryption)" + fi + + if [[ -n "${AGE_IDENTITY_FILE:-}" ]]; then + if [[ -f "$AGE_IDENTITY_FILE" ]]; then + log_info "AGE_IDENTITY_FILE exists: $AGE_IDENTITY_FILE" + else + log_warn "AGE_IDENTITY_FILE missing: $AGE_IDENTITY_FILE" + fi + else + log_warn "AGE_IDENTITY_FILE not set (required for restore drill)" + fi + + log_info "=== Disk Space ===" + local avail + avail=$(df -P "$OUTPUT_DIR" | awk 'NR==2 {print $4}') + log_info "Available space in $OUTPUT_DIR: $((avail / 1024)) MB" + + log_info "=== Parameters ===" + log_info "NODE_NAME=${NODE_NAME:-node-a}" + log_info "BACKUP_LABEL=${BACKUP_LABEL:-manual}" + log_info "BACKUP_EXCLUDES=$BACKUP_EXCLUDES" + log_info "DRY_RUN=${DRY_RUN:-1} (apply scripts require DRY_RUN=0)" + + if [[ $missing -gt 0 ]]; then + die "Missing $missing required tools. Install them before proceeding." + fi + + log_info "Preflight OK." + log_info "Completed $SCRIPT_NAME" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/10_backup_plan.sh b/backup-sovereign/scripts/10_backup_plan.sh new file mode 100755 index 0000000..97285cb --- /dev/null +++ b/backup-sovereign/scripts/10_backup_plan.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${NODE_NAME:=node-a}" +: "${BACKUP_LABEL:=manual}" +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${BACKUP_SOURCES:=}" +: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}" + +# === FUNCTIONS === +log_plan() { echo "[PLAN] $(date -Iseconds) $*"; } +die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; } + +estimate_size() { + local total=0 + IFS=',' read -r -a sources <<< "$BACKUP_SOURCES" + IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES" + + for src in "${sources[@]}"; do + src="${src/#\~/$HOME}" + if [[ -e "$src" ]]; then + # Build find exclude args + local find_excludes=() + for ex in "${excludes[@]}"; do + find_excludes+=(-name "$ex" -prune -o) + done + + local size + size=$(find "$src" "${find_excludes[@]}" -type f -print0 2>/dev/null | \ + xargs -0 stat -c%s 2>/dev/null | \ + awk '{sum+=$1} END {print sum+0}') + total=$((total + size)) + fi + done + echo "$total" +} + +main() { + [[ -n "$BACKUP_SOURCES" ]] || die "BACKUP_SOURCES is required (comma-separated paths)." + + local ts run_id run_dir + ts="$(date -Iseconds | tr ':' '-')" + run_id="${NODE_NAME}_${BACKUP_LABEL}_${ts}" + run_dir="$OUTPUT_DIR/runs/$run_id" + + log_plan "=== Backup Plan ===" + log_plan "Run ID: $run_id" + log_plan "Run directory: $run_dir" + log_plan "Archive: $run_dir/archive.tar.gz" + log_plan "Manifest: $run_dir/manifest.json" + echo "" + + log_plan "=== Sources ===" + IFS=',' read -r -a sources <<< "$BACKUP_SOURCES" + for src in "${sources[@]}"; do + src="${src/#\~/$HOME}" + if [[ -e "$src" ]]; then + log_plan " [OK] $src" + else + log_plan " [MISSING] $src" + fi + done + echo "" + + log_plan "=== Excludes ===" + IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES" + for ex in "${excludes[@]}"; do + log_plan " - $ex" + done + echo "" + + log_plan "=== Size Estimate ===" + local est_bytes est_mb + est_bytes=$(estimate_size) + est_mb=$((est_bytes / 1024 / 1024)) + log_plan "Estimated uncompressed: ${est_mb} MB ($est_bytes bytes)" + log_plan "Compressed size will be smaller (typically 30-70% of original)" + echo "" + + log_plan "Next: ./scripts/11_backup_apply.sh (requires DRY_RUN=0)" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/11_backup_apply.sh b/backup-sovereign/scripts/11_backup_apply.sh new file mode 100755 index 0000000..0616f1c --- /dev/null +++ b/backup-sovereign/scripts/11_backup_apply.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${NODE_NAME:=node-a}" +: "${BACKUP_LABEL:=manual}" +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${BACKUP_SOURCES:=}" +: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}" +: "${DRY_RUN:=1}" +: "${REQUIRE_CONFIRM:=1}" +: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +b3_file() { + if command -v b3sum &>/dev/null; then + b3sum "$1" | awk '{print $1}' + else + blake3 "$1" + fi +} + +require_confirm() { + [[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)." + + if [[ "$REQUIRE_CONFIRM" == "1" ]]; then + echo "" + echo "CONFIRMATION REQUIRED" + echo "Type the phrase exactly to continue:" + echo " $CONFIRM_PHRASE" + read -r input + [[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting." + fi +} + +json_array() { + # Convert comma-separated string to JSON array + local input="$1" + local first=true + echo -n "[" + IFS=',' read -r -a items <<< "$input" + for item in "${items[@]}"; do + if [[ "$first" == "true" ]]; then + first=false + else + echo -n "," + fi + echo -n "\"$item\"" + done + echo -n "]" +} + +main() { + [[ -n "$BACKUP_SOURCES" ]] || die "BACKUP_SOURCES is required (comma-separated paths)." + + require_confirm + + local ts run_id run_dir archive excludes_file manifest + ts="$(date -Iseconds | tr ':' '-')" + run_id="${NODE_NAME}_${BACKUP_LABEL}_${ts}" + run_dir="$OUTPUT_DIR/runs/$run_id" + archive="$run_dir/archive.tar.gz" + excludes_file="$run_dir/excludes.txt" + manifest="$run_dir/manifest.json" + + mkdir -p "$run_dir" + + # Write excludes file + log_info "Writing excludes: $excludes_file" + : > "$excludes_file" + IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES" + for ex in "${excludes[@]}"; do + echo "$ex" >> "$excludes_file" + done + + # Build tar exclude args + local tar_excludes=() + while IFS= read -r pat; do + [[ -n "$pat" ]] && tar_excludes+=("--exclude=$pat") + done < "$excludes_file" + + # Expand sources + local expanded_sources=() + IFS=',' read -r -a sources <<< "$BACKUP_SOURCES" + for src in "${sources[@]}"; do + expanded_sources+=("${src/#\~/$HOME}") + done + + # Create archive + log_info "Creating archive: $archive" + tar -czf "$archive" "${tar_excludes[@]}" "${expanded_sources[@]}" + + local archive_size archive_b3 + archive_size=$(stat -c%s "$archive") + archive_b3=$(b3_file "$archive") + + log_info "Archive size: $archive_size bytes" + log_info "Archive BLAKE3: $archive_b3" + + # Create manifest (pure bash JSON) + log_info "Writing manifest: $manifest" + cat > "$manifest" < "$OUTPUT_DIR/last_run_dir.txt" + log_info "Saved last run pointer: $OUTPUT_DIR/last_run_dir.txt" + + log_info "Backup complete." + log_info "Next: ./scripts/20_encrypt_plan.sh then 21_encrypt_apply.sh" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/20_encrypt_plan.sh b/backup-sovereign/scripts/20_encrypt_plan.sh new file mode 100755 index 0000000..626bd36 --- /dev/null +++ b/backup-sovereign/scripts/20_encrypt_plan.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${AGE_RECIPIENT_FILE:=}" + +# === FUNCTIONS === +log_plan() { echo "[PLAN] $(date -Iseconds) $*"; } +die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; } + +main() { + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + [[ -d "$run_dir" ]] || die "Run directory missing: $run_dir" + [[ -f "$run_dir/archive.tar.gz" ]] || die "Archive missing: $run_dir/archive.tar.gz" + + log_plan "=== Encryption Plan ===" + log_plan "Method: age" + log_plan "Run directory: $run_dir" + log_plan "Input: $run_dir/archive.tar.gz" + log_plan "Output: $run_dir/archive.tar.gz.age" + echo "" + + log_plan "=== Recipient File ===" + if [[ -n "$AGE_RECIPIENT_FILE" ]]; then + if [[ -f "$AGE_RECIPIENT_FILE" ]]; then + log_plan "File: $AGE_RECIPIENT_FILE" + log_plan "Recipients:" + while IFS= read -r line; do + [[ "$line" =~ ^# ]] && continue + [[ -z "$line" ]] && continue + # Show truncated public key + log_plan " - ${line:0:20}..." + done < "$AGE_RECIPIENT_FILE" + else + log_plan "[MISSING] $AGE_RECIPIENT_FILE" + fi + else + log_plan "[NOT SET] AGE_RECIPIENT_FILE required for encryption" + fi + echo "" + + log_plan "=== Archive Info ===" + local size + size=$(stat -c%s "$run_dir/archive.tar.gz") + log_plan "Archive size: $size bytes ($((size / 1024 / 1024)) MB)" + echo "" + + log_plan "Next: ./scripts/21_encrypt_apply.sh (requires DRY_RUN=0)" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/21_encrypt_apply.sh b/backup-sovereign/scripts/21_encrypt_apply.sh new file mode 100755 index 0000000..032dc86 --- /dev/null +++ b/backup-sovereign/scripts/21_encrypt_apply.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${AGE_RECIPIENT_FILE:=}" +: "${DRY_RUN:=1}" +: "${REQUIRE_CONFIRM:=1}" +: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +require_confirm() { + [[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)." + + if [[ "$REQUIRE_CONFIRM" == "1" ]]; then + echo "" + echo "CONFIRMATION REQUIRED" + echo "Type the phrase exactly to continue:" + echo " $CONFIRM_PHRASE" + read -r input + [[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting." + fi +} + +main() { + require_confirm + + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + local archive="$run_dir/archive.tar.gz" + [[ -f "$archive" ]] || die "Missing archive: $archive" + + [[ -n "$AGE_RECIPIENT_FILE" ]] || die "AGE_RECIPIENT_FILE is required for encryption." + [[ -f "$AGE_RECIPIENT_FILE" ]] || die "AGE_RECIPIENT_FILE not found: $AGE_RECIPIENT_FILE" + + local encrypted="$run_dir/archive.tar.gz.age" + + log_info "Encrypting with age..." + log_info "Input: $archive" + log_info "Output: $encrypted" + log_info "Recipients: $AGE_RECIPIENT_FILE" + + age -R "$AGE_RECIPIENT_FILE" -o "$encrypted" "$archive" + + local enc_size + enc_size=$(stat -c%s "$encrypted") + log_info "Encrypted size: $enc_size bytes" + + log_info "Encryption complete." + log_info "Next: ./scripts/30_generate_proof.sh" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/30_generate_proof.sh b/backup-sovereign/scripts/30_generate_proof.sh new file mode 100755 index 0000000..9496380 --- /dev/null +++ b/backup-sovereign/scripts/30_generate_proof.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${NODE_NAME:=node-a}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +b3_file() { + if command -v b3sum &>/dev/null; then + b3sum "$1" | awk '{print $1}' + else + blake3 "$1" + fi +} + +b3_string() { + if command -v b3sum &>/dev/null; then + echo -n "$1" | b3sum | awk '{print $1}' + else + echo -n "$1" | blake3 + fi +} + +main() { + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + local manifest="$run_dir/manifest.json" + [[ -f "$manifest" ]] || die "Missing manifest: $manifest" + + # Find encrypted archive + local encrypted="" + if [[ -f "$run_dir/archive.tar.gz.age" ]]; then + encrypted="$run_dir/archive.tar.gz.age" + else + die "Missing encrypted archive. Run 21_encrypt_apply.sh first." + fi + + log_info "Generating proof receipts..." + log_info "Run directory: $run_dir" + + # Compute BLAKE3 hashes + local manifest_b3 encrypted_b3 + manifest_b3=$(b3_file "$manifest") + encrypted_b3=$(b3_file "$encrypted") + + log_info "Manifest BLAKE3: $manifest_b3" + log_info "Encrypted BLAKE3: $encrypted_b3" + + # Compute ROOT = BLAKE3(manifest_b3 || encrypted_b3) + # Using stable text concatenation + local concat root_b3 + concat="${manifest_b3}${encrypted_b3}" + root_b3=$(b3_string "$concat") + + log_info "ROOT BLAKE3: $root_b3" + + # Write ROOT.txt + echo "$root_b3" > "$run_dir/ROOT.txt" + log_info "Wrote: $run_dir/ROOT.txt" + + # Write PROOF.json + cat > "$run_dir/PROOF.json" <&2; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +check_file() { + local path="$1" + local name="$2" + if [[ -f "$path" ]]; then + local size + size=$(stat -c%s "$path") + log_info "[OK] $name ($size bytes)" + return 0 + else + log_warn "[MISSING] $name" + return 1 + fi +} + +main() { + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + log_info "Verifying backup artifacts..." + log_info "Run directory: $run_dir" + echo "" + + local missing=0 + + log_info "=== Core Artifacts ===" + check_file "$run_dir/archive.tar.gz" "archive.tar.gz" || ((missing++)) + check_file "$run_dir/archive.tar.gz.age" "archive.tar.gz.age" || ((missing++)) + check_file "$run_dir/manifest.json" "manifest.json" || ((missing++)) + echo "" + + log_info "=== Proof Artifacts ===" + check_file "$run_dir/ROOT.txt" "ROOT.txt" || ((missing++)) + check_file "$run_dir/PROOF.json" "PROOF.json" || ((missing++)) + echo "" + + log_info "=== Metadata ===" + check_file "$run_dir/excludes.txt" "excludes.txt" || true + echo "" + + if [[ $missing -gt 0 ]]; then + die "Verification failed: $missing missing artifacts." + fi + + log_info "=== ROOT Value ===" + local root + root=$(cat "$run_dir/ROOT.txt") + log_info "ROOT: $root" + echo "" + + log_info "Verification complete. All artifacts present." + log_info "Next: ./scripts/50_restore_drill.sh (MANDATORY)" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/50_restore_drill.sh b/backup-sovereign/scripts/50_restore_drill.sh new file mode 100755 index 0000000..5735f4e --- /dev/null +++ b/backup-sovereign/scripts/50_restore_drill.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${AGE_IDENTITY_FILE:=}" +: "${DRY_RUN:=1}" +: "${REQUIRE_CONFIRM:=1}" +: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +require_confirm() { + [[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)." + + if [[ "$REQUIRE_CONFIRM" == "1" ]]; then + echo "" + echo "RESTORE DRILL - CONFIRMATION REQUIRED" + echo "This will decrypt and extract the backup to verify recoverability." + echo "Type the phrase exactly to continue:" + echo " $CONFIRM_PHRASE" + read -r input + [[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting." + fi +} + +main() { + require_confirm + + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + # Find encrypted archive + local encrypted="" + if [[ -f "$run_dir/archive.tar.gz.age" ]]; then + encrypted="$run_dir/archive.tar.gz.age" + else + die "Missing encrypted archive. Run 21_encrypt_apply.sh first." + fi + + [[ -n "$AGE_IDENTITY_FILE" ]] || die "AGE_IDENTITY_FILE is required for restore drill." + [[ -f "$AGE_IDENTITY_FILE" ]] || die "AGE_IDENTITY_FILE not found: $AGE_IDENTITY_FILE" + + # Create temp directory for restore + local restore_dir + restore_dir=$(mktemp -d) + log_info "Restore drill temp directory: $restore_dir" + + local decrypted="$restore_dir/archive.tar.gz" + + # Decrypt + log_info "Decrypting archive..." + age -d -i "$AGE_IDENTITY_FILE" -o "$decrypted" "$encrypted" + log_info "Decryption successful." + + # Extract + log_info "Extracting archive..." + mkdir -p "$restore_dir/extract" + tar -xzf "$decrypted" -C "$restore_dir/extract" + + # Count files + local file_count + file_count=$(find "$restore_dir/extract" -type f | wc -l | tr -d ' ') + + if [[ "$file_count" -eq 0 ]]; then + die "Restore drill FAILED: No files extracted." + fi + + log_info "Extracted $file_count files." + + # Save restore pointer + echo "$restore_dir" > "$run_dir/last_restore_dir.txt" + log_info "Saved restore pointer: $run_dir/last_restore_dir.txt" + + echo "" + log_info "==========================================" + log_info " RESTORE DRILL: PASSED" + log_info " Files restored: $file_count" + log_info " Location: $restore_dir/extract" + log_info "==========================================" + echo "" + + log_info "Restore drill complete." + log_info "Next: ./scripts/90_verify.sh then ./scripts/99_report.sh" +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/90_verify.sh b/backup-sovereign/scripts/90_verify.sh new file mode 100755 index 0000000..a16b25d --- /dev/null +++ b/backup-sovereign/scripts/90_verify.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" +CHECKS_DIR="$SKILL_ROOT/checks" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" +: "${NODE_NAME:=node-a}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; } + +run_check() { + local script="$1" + if [[ -x "$CHECKS_DIR/$script" ]]; then + if "$CHECKS_DIR/$script" &>/dev/null; then + echo "true" + else + echo "false" + fi + else + echo "skip" + fi +} + +main() { + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + [[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first." + + local run_dir + run_dir="$(cat "$last_run_file")" + + mkdir -p "$OUTPUT_DIR" + local status="$OUTPUT_DIR/status_matrix.json" + + # Check artifacts + local has_archive has_encrypted has_manifest has_proof has_root has_restore + + [[ -f "$run_dir/archive.tar.gz" ]] && has_archive="true" || has_archive="false" + [[ -f "$run_dir/archive.tar.gz.age" ]] && has_encrypted="true" || has_encrypted="false" + [[ -f "$run_dir/manifest.json" ]] && has_manifest="true" || has_manifest="false" + [[ -f "$run_dir/PROOF.json" ]] && has_proof="true" || has_proof="false" + [[ -f "$run_dir/ROOT.txt" ]] && has_root="true" || has_root="false" + [[ -f "$run_dir/last_restore_dir.txt" ]] && has_restore="true" || has_restore="false" + + # Run check scripts + local tools_ok space_ok restore_ok + tools_ok=$(run_check "check_tools.sh") + space_ok=$(run_check "check_space.sh") + restore_ok=$(run_check "check_restore.sh") + + # Determine blockers and warnings + local blockers="" warnings="" next_steps="" + + if [[ "$has_restore" == "false" ]]; then + blockers="${blockers}\"Restore drill not completed\"," + fi + if [[ "$has_encrypted" == "false" ]]; then + blockers="${blockers}\"Archive not encrypted\"," + fi + if [[ "$has_manifest" == "false" ]]; then + warnings="${warnings}\"Manifest missing\"," + fi + if [[ "$has_proof" == "false" ]]; then + warnings="${warnings}\"Proof receipts missing\"," + fi + + # Determine next steps + if [[ "$has_restore" == "true" && "$has_encrypted" == "true" ]]; then + next_steps="${next_steps}\"Store encrypted bundle off-node\"," + next_steps="${next_steps}\"Anchor ROOT.txt with rfc3161-anchor\"," + next_steps="${next_steps}\"Proceed to disaster-recovery skill\"," + else + if [[ "$has_encrypted" == "false" ]]; then + next_steps="${next_steps}\"Run 21_encrypt_apply.sh\"," + fi + if [[ "$has_restore" == "false" ]]; then + next_steps="${next_steps}\"Run 50_restore_drill.sh (MANDATORY)\"," + fi + fi + + # Remove trailing commas + blockers="[${blockers%,}]" + warnings="[${warnings%,}]" + next_steps="[${next_steps%,}]" + + # Get ROOT value if exists + local root_value="null" + if [[ -f "$run_dir/ROOT.txt" ]]; then + root_value="\"$(cat "$run_dir/ROOT.txt")\"" + fi + + cat > "$status" < "$report" <&2; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +main() { + if [[ ! -d "$OUTPUT_DIR" ]]; then + log_info "Output directory does not exist. Nothing to purge." + exit 0 + fi + + local run_count=0 + if [[ -d "$OUTPUT_DIR/runs" ]]; then + run_count=$(find "$OUTPUT_DIR/runs" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + fi + + log_warn "This will PERMANENTLY DELETE all backup outputs:" + log_warn " Directory: $OUTPUT_DIR" + log_warn " Backup runs: $run_count" + log_warn "" + log_warn "This action cannot be undone!" + echo "" + echo "Type 'PURGE ALL' to confirm:" + read -r confirm + [[ "$confirm" == "PURGE ALL" ]] || die "Aborted." + + # Clean up any restore drill temp directories + if [[ -d "$OUTPUT_DIR/runs" ]]; then + for run_dir in "$OUTPUT_DIR/runs"/*; do + if [[ -f "$run_dir/last_restore_dir.txt" ]]; then + local restore_dir + restore_dir="$(cat "$run_dir/last_restore_dir.txt")" + if [[ -d "$restore_dir" ]]; then + log_info "Removing restore temp: $restore_dir" + rm -rf "$restore_dir" + fi + fi + done + fi + + log_info "Purging outputs directory..." + rm -rf "$OUTPUT_DIR"/* + + log_info "Purge complete." +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/scripts/rollback/undo_last_backup.sh b/backup-sovereign/scripts/rollback/undo_last_backup.sh new file mode 100755 index 0000000..9ede995 --- /dev/null +++ b/backup-sovereign/scripts/rollback/undo_last_backup.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === METADATA === +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" + +# === CONFIGURATION === +: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" + +# === FUNCTIONS === +log_info() { echo "[INFO] $(date -Iseconds) $*"; } +log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; } +log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } +die() { log_error "$@"; exit 1; } + +main() { + local last_run_file="$OUTPUT_DIR/last_run_dir.txt" + + if [[ ! -f "$last_run_file" ]]; then + log_warn "No last run pointer found. Nothing to undo." + exit 0 + fi + + local run_dir + run_dir="$(cat "$last_run_file")" + + if [[ ! -d "$run_dir" ]]; then + log_warn "Run directory does not exist: $run_dir" + rm -f "$last_run_file" + exit 0 + fi + + log_warn "This will remove the last backup run:" + log_warn " $run_dir" + echo "" + echo "Type 'DELETE' to confirm:" + read -r confirm + [[ "$confirm" == "DELETE" ]] || die "Aborted." + + # Clean up restore drill temp directory if it exists + if [[ -f "$run_dir/last_restore_dir.txt" ]]; then + local restore_dir + restore_dir="$(cat "$run_dir/last_restore_dir.txt")" + if [[ -d "$restore_dir" ]]; then + log_info "Removing restore drill temp: $restore_dir" + rm -rf "$restore_dir" + fi + fi + + log_info "Removing run directory: $run_dir" + rm -rf "$run_dir" + + log_info "Removing last run pointer" + rm -f "$last_run_file" + + log_info "Undo complete." +} + +[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" diff --git a/backup-sovereign/templates/manifest.schema.json b/backup-sovereign/templates/manifest.schema.json new file mode 100644 index 0000000..82b0d43 --- /dev/null +++ b/backup-sovereign/templates/manifest.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Backup Manifest", + "description": "Schema for backup-sovereign manifest.json files", + "type": "object", + "required": ["version", "node", "label", "run_id", "created_at", "sources", "archive"], + "properties": { + "version": { + "type": "integer", + "description": "Manifest schema version", + "const": 1 + }, + "node": { + "type": "string", + "description": "Node identifier" + }, + "label": { + "type": "string", + "description": "Backup label (e.g., daily, weekly, manual)" + }, + "run_id": { + "type": "string", + "description": "Unique run identifier (node_label_timestamp)" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of backup creation" + }, + "sources": { + "type": "array", + "items": { "type": "string" }, + "description": "List of source paths included in backup" + }, + "excludes": { + "type": "array", + "items": { "type": "string" }, + "description": "List of exclude patterns" + }, + "archive": { + "type": "object", + "required": ["path", "bytes", "blake3"], + "properties": { + "path": { + "type": "string", + "description": "Relative path to archive file" + }, + "bytes": { + "type": "integer", + "description": "Archive size in bytes" + }, + "blake3": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "BLAKE3 hash of archive" + } + } + } + } +} diff --git a/btc-anchor/SKILL.md b/btc-anchor/SKILL.md new file mode 100644 index 0000000..f7fda2d --- /dev/null +++ b/btc-anchor/SKILL.md @@ -0,0 +1,66 @@ +--- +name: btc-anchor +description: > + Anchor a Merkle root (root_hex) to Bitcoin testnet or mainnet using OP_RETURN via bitcoin-cli. + Emits PROOF.json + tx metadata with plan/apply/rollback and verification. + Consumes merkle-forest ROOT.txt (or explicit ROOT_HEX). Triggers: 'btc anchor', + 'anchor root on bitcoin', 'op_return', 'taproot proof', 'bitcoin-cli'. +version: 1.0.0 +--- + +# BTC Anchor (OP_RETURN via bitcoin-cli) + +This skill anchors a **root_hex** on Bitcoin by creating a transaction +with an **OP_RETURN** output containing the root bytes. + +## Requirements +- `bitcoin-cli` connected to a synced node (mainnet/testnet/signet) +- Wallet loaded + funded (UTXOs) +- Network parameters set (v1 uses `bitcoin-cli -testnet` / `-signet` flags) + +## Quick Start + +```bash +cd ~/.claude/skills/btc-anchor + +export ROOT_FILE="$HOME/.claude/skills/merkle-forest/outputs/runs//ROOT.txt" +export BTC_NETWORK="testnet" # mainnet|testnet|signet +export BTC_FEE_RATE="5" # sat/vB (rough) +export OP_RETURN_PREFIX="VM" # 2-byte ascii prefix + +./scripts/00_preflight.sh +./scripts/10_plan.sh + +export DRY_RUN=0 +./scripts/11_apply.sh + +./scripts/90_verify.sh +./scripts/99_report.sh +``` + +## Inputs + +| Parameter | Required | Default | Description | +|---|---:|---|---| +| ROOT_FILE | No | (empty) | ROOT.txt path | +| ROOT_HEX | No | (empty) | Explicit root hex (overrides ROOT_FILE) | +| BTC_NETWORK | No | testnet | mainnet/testnet/signet | +| BTC_FEE_RATE | No | 5 | sat/vB (passed to walletcreatefundedpsbt) | +| OP_RETURN_PREFIX | No | VM | ASCII prefix (helps identify payloads) | +| DRY_RUN | No | 1 | Apply refuses unless DRY_RUN=0 | +| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase | +| CONFIRM_PHRASE | No | I UNDERSTAND THIS WILL BROADCAST A BITCOIN TX | Safety phrase | + +## Outputs +`outputs/runs/