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 <noreply@anthropic.com>
This commit is contained in:
Vault Sovereign
2025-12-27 00:25:00 +00:00
commit eac77ef7b4
213 changed files with 11724 additions and 0 deletions

177
backup-sovereign/SKILL.md Normal file
View File

@@ -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/<run_id>/archive.tar.gz` | Unencrypted archive |
| `outputs/runs/<run_id>/archive.tar.gz.age` | Encrypted archive |
| `outputs/runs/<run_id>/manifest.json` | File list + sizes + BLAKE3 hashes |
| `outputs/runs/<run_id>/ROOT.txt` | BLAKE3 root (for anchoring) |
| `outputs/runs/<run_id>/PROOF.json` | Metadata receipt |
| `outputs/runs/<run_id>/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

View File

@@ -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" ]]

View File

@@ -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 ]]

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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" <<EOF
{
"version": 1,
"node": "$NODE_NAME",
"label": "$BACKUP_LABEL",
"run_id": "$run_id",
"created_at": "$(date -Iseconds)",
"sources": $(json_array "$BACKUP_SOURCES"),
"excludes": $(json_array "$BACKUP_EXCLUDES"),
"archive": {
"path": "archive.tar.gz",
"bytes": $archive_size,
"blake3": "$archive_b3"
}
}
EOF
# Save last run pointer
echo "$run_dir" > "$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 "$@"

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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" <<EOF
{
"version": 1,
"node": "$NODE_NAME",
"created_at": "$(date -Iseconds)",
"artifacts": {
"manifest_blake3": "$manifest_b3",
"encrypted_archive_blake3": "$encrypted_b3",
"root_blake3": "$root_b3"
},
"computation": "ROOT = BLAKE3(manifest_blake3 || encrypted_archive_blake3)",
"notes": "ROOT can be anchored via merkle-forest/rfc3161-anchor skills."
}
EOF
log_info "Wrote: $run_dir/PROOF.json"
log_info "Proof generation complete."
log_info "Next: ./scripts/40_verify_backup.sh"
}
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"

View File

@@ -0,0 +1,74 @@
#!/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}"
# === 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_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 "$@"

View File

@@ -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 "$@"

View File

@@ -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" <<EOF
{
"skill": "backup-sovereign",
"node": "$NODE_NAME",
"timestamp": "$(date -Iseconds)",
"run_dir": "$run_dir",
"root": $root_value,
"checks": {
"archive": $has_archive,
"encrypted": $has_encrypted,
"manifest": $has_manifest,
"proof": $has_proof,
"root": $has_root,
"restore_drill": $has_restore,
"tools": $tools_ok,
"space": $space_ok
},
"blockers": $blockers,
"warnings": $warnings,
"next_steps": $next_steps
}
EOF
log_info "Wrote status matrix: $status"
echo ""
echo "============================================"
echo " VERIFICATION SUMMARY"
echo "============================================"
echo ""
echo " Archive: $has_archive"
echo " Encrypted: $has_encrypted"
echo " Manifest: $has_manifest"
echo " Proof: $has_proof"
echo " ROOT: $has_root"
echo " Restore Drill: $has_restore"
echo ""
# Return success only if restore drill passed
[[ "$has_restore" == "true" ]]
}
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"

View File

@@ -0,0 +1,157 @@
#!/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) $*"; }
get_file_size() {
local path="$1"
if [[ -f "$path" ]]; then
stat -c%s "$path"
else
echo "0"
fi
}
main() {
mkdir -p "$OUTPUT_DIR"
local report="$OUTPUT_DIR/audit_report.md"
local status="$OUTPUT_DIR/status_matrix.json"
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
local run_dir="(unknown)"
[[ -f "$last_run_file" ]] && run_dir="$(cat "$last_run_file")"
local root_value="(not generated)"
[[ -f "$run_dir/ROOT.txt" ]] && root_value="$(cat "$run_dir/ROOT.txt")"
local archive_size enc_size
archive_size=$(get_file_size "$run_dir/archive.tar.gz")
enc_size=$(get_file_size "$run_dir/archive.tar.gz.age")
local restore_status="NOT COMPLETED"
[[ -f "$run_dir/last_restore_dir.txt" ]] && restore_status="PASSED"
cat > "$report" <<EOF
# Backup Sovereign Audit Report
**Generated:** $(date -Iseconds)
**Node:** $NODE_NAME
**Run Directory:** $run_dir
**Skill Version:** 1.0.0
---
## Executive Summary
This report documents the backup operations performed on **$NODE_NAME**
for sovereign EU infrastructure.
---
## Backup Artifacts
| Artifact | Path | Size |
|----------|------|------|
| Archive | archive.tar.gz | $archive_size bytes |
| Encrypted | archive.tar.gz.age | $enc_size bytes |
| Manifest | manifest.json | $(get_file_size "$run_dir/manifest.json") bytes |
| Proof | PROOF.json | $(get_file_size "$run_dir/PROOF.json") bytes |
| ROOT | ROOT.txt | $(get_file_size "$run_dir/ROOT.txt") bytes |
---
## Proof Receipt
| Field | Value |
|-------|-------|
| ROOT (BLAKE3) | \`$root_value\` |
This ROOT value can be anchored via:
- merkle-forest skill (aggregate with other proofs)
- rfc3161-anchor skill (RFC 3161 timestamp)
- eth-anchor / btc-anchor skills (blockchain anchoring)
---
## Restore Drill
| Status | Result |
|--------|--------|
| Restore Drill | **$restore_status** |
$(if [[ -f "$run_dir/last_restore_dir.txt" ]]; then
echo "Restore location: \`$(cat "$run_dir/last_restore_dir.txt")\`"
else
echo "**WARNING:** Backup has not been verified via restore drill."
echo "Run \`./scripts/50_restore_drill.sh\` to validate recoverability."
fi)
---
## Verification Results
$(if [[ -f "$status" ]]; then
echo '```json'
cat "$status"
echo '```'
else
echo "Status matrix not found. Run 90_verify.sh first."
fi)
---
## EU Compliance Declaration
| Aspect | Value |
|--------|-------|
| Data Residency | EU (Ireland - Dublin) |
| GDPR Applicable | Yes (depends on backup content) |
| Jurisdiction | Irish Law |
| Encryption at Rest | Yes (age) |
---
## Next Steps
1. Store encrypted bundle off-node (secondary disk / object store)
2. Test restore on a different machine (recommended)
3. Anchor ROOT.txt with rfc3161-anchor skill
4. Proceed to **disaster-recovery** skill
---
## Artifact Locations
| Artifact | Path |
|----------|------|
| Archive | $run_dir/archive.tar.gz |
| Encrypted Archive | $run_dir/archive.tar.gz.age |
| Manifest | $run_dir/manifest.json |
| ROOT | $run_dir/ROOT.txt |
| Proof | $run_dir/PROOF.json |
| Status Matrix | $OUTPUT_DIR/status_matrix.json |
| This Report | $OUTPUT_DIR/audit_report.md |
---
*Report generated by backup-sovereign skill v1.0.0*
*$(date -Iseconds)*
EOF
log_info "Audit report written to $report"
echo ""
cat "$report"
log_info "Completed $SCRIPT_NAME"
}
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"

View File

@@ -0,0 +1,59 @@
#!/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() {
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 "$@"

View File

@@ -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 "$@"

View File

@@ -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"
}
}
}
}
}