Initial commit: Git OpenTimestamp hooks skill

This commit is contained in:
Otto 2026-03-08 00:12:45 +01:00
commit eec64d16c6
13 changed files with 1145 additions and 0 deletions

165
scripts/backfill-proofs.sh Executable file
View file

@ -0,0 +1,165 @@
#!/bin/bash
# backfill-proofs.sh - Generate/upgrade OpenTimestamp proofs for all commits
# Usage: backfill-proofs.sh [repository-path]
set -e
REPO_PATH="${1:-.}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_PATH"
# Verify we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: not a git repository" >&2
exit 1
fi
# Find the generate-proof.sh script
GENERATE_SCRIPT=""
# Check if generate-proof.sh is in the same directory as this script
if [ -f "$SCRIPT_DIR/generate-proof.sh" ]; then
GENERATE_SCRIPT="$SCRIPT_DIR/generate-proof.sh"
# Check in repository root
elif [ -f "generate-proof.sh" ]; then
GENERATE_SCRIPT="./generate-proof.sh"
# Check if skill is installed globally
elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" ]; then
GENERATE_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh"
else
echo "Error: generate-proof.sh not found" >&2
exit 1
fi
chmod +x "$GENERATE_SCRIPT"
# Ensure .ots directory exists
mkdir -p .ots
# Cache file for attestation status (avoids repeated ots info calls)
STATUS_CACHE=".ots/.attestation-cache"
if [ ! -f "$STATUS_CACHE" ]; then
echo "# Attestation status cache" > "$STATUS_CACHE"
echo "# Format: commit-hash:status:timestamp" >> "$STATUS_CACHE"
fi
# Function to check cached status
get_cached_status() {
local commit="$1"
local cache_line=$(grep "^$commit:" "$STATUS_CACHE" 2>/dev/null | tail -1)
if [ -n "$cache_line" ]; then
local status=$(echo "$cache_line" | cut -d: -f2)
local timestamp=$(echo "$cache_line" | cut -d: -f3)
local now=$(date +%s)
local age=$((now - timestamp))
# Cache valid for 1 hour (3600 seconds)
if [ "$age" -lt 3600 ]; then
echo "$status"
return 0
fi
fi
return 1
}
# Function to cache status
cache_status() {
local commit="$1"
local status="$2"
local now=$(date +%s)
echo "$commit:$status:$now" >> "$STATUS_CACHE"
}
echo "[ots-backfill] Scanning commit history..."
# Get all commit hashes (oldest to newest)
COMMITS=$(git rev-list --reverse HEAD)
TOTAL=$(echo "$COMMITS" | wc -l)
CURRENT=0
# Track if any proofs were updated
UPDATED=0
for COMMIT in $COMMITS; do
CURRENT=$((CURRENT + 1))
echo "[ots-backfill] Processing commit $CURRENT/$TOTAL: ${COMMIT:0:8}"
# Check if proof exists for this commit
PROOF_FILE=".ots/${COMMIT}.ots"
if [ -f "$PROOF_FILE" ]; then
# Check cached status first
CACHED_STATUS=$(get_cached_status "$COMMIT" || echo "")
if [ "$CACHED_STATUS" = "attested" ]; then
echo " ✓ Already attested (cached, skipping)"
continue
fi
# Check if already fully attested (only if not cached or cache expired)
PENDING_COUNT=$(ots info "$PROOF_FILE" 2>&1 | grep -c "PendingAttestation" || echo "0")
if [ "$PENDING_COUNT" -eq 0 ]; then
echo " ✓ Already attested"
cache_status "$COMMIT" "attested"
continue
fi
# Cache as pending
cache_status "$COMMIT" "pending"
# Skip upgrade if cache is fresh (< 10 min old)
CACHE_LINE=$(grep "^$COMMIT:" "$STATUS_CACHE" | tail -1)
CACHE_TIME=$(echo "$CACHE_LINE" | cut -d: -f3)
NOW=$(date +%s)
CACHE_AGE=$((NOW - CACHE_TIME))
if [ "$CACHE_AGE" -lt 600 ]; then
echo " - Pending (cached <10min, skipping calendar call)"
continue
fi
# Try to upgrade existing proof (contacts calendars)
echo " Upgrading pending proof..."
if ots upgrade "$PROOF_FILE" 2>/dev/null; then
echo " ✓ Upgraded to attested"
cache_status "$COMMIT" "attested"
UPDATED=$((UPDATED + 1))
else
echo " - Still pending (no upgrade available yet)"
# Update cache timestamp
cache_status "$COMMIT" "pending"
fi
else
# Generate new proof for this commit
echo " Generating new proof..."
"$GENERATE_SCRIPT" "$COMMIT" "$PROOF_FILE" >/dev/null 2>&1
echo " ✓ Generated proof"
cache_status "$COMMIT" "pending"
UPDATED=$((UPDATED + 1))
fi
done
# Create/update latest proof symlink
LATEST_COMMIT=$(git rev-parse HEAD)
if [ -f ".ots/${LATEST_COMMIT}.ots" ]; then
cp ".ots/${LATEST_COMMIT}.ots" ".ots/proof.ots"
echo "[ots-backfill] Updated .ots/proof.ots for latest commit"
fi
# Save previous commit chain info
echo "[ots-backfill] Saving commit chain..."
git rev-list HEAD | while read COMMIT; do
PREV=$(git rev-parse ${COMMIT}^1 2>/dev/null || echo "")
if [ -n "$PREV" ]; then
echo "$COMMIT:$PREV" >> ".ots/commit-chain.txt"
fi
done
echo ""
echo "[ots-backfill] Complete! Processed $TOTAL commits, updated $UPDATED proofs"
echo ""
echo "Proofs stored in: .ots/"
echo " - Individual proofs: .ots/<commit-hash>.ots"
echo " - Latest proof: .ots/proof.ots"
echo " - Commit chain: .ots/commit-chain.txt"

20
scripts/check-attestation.sh Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
# check-attestation.sh - Check if an OpenTimestamp proof is fully attested
# Usage: check-attestation.sh <proof-file>
# Returns: 0 if fully attested, 1 if pending
PROOF_FILE="${1:-}"
if [ -z "$PROOF_FILE" ] || [ ! -f "$PROOF_FILE" ]; then
echo "Error: proof file not found: $PROOF_FILE" >&2
exit 2
fi
# Check for PendingAttestation in ots info output
if ots info "$PROOF_FILE" 2>&1 | grep -q "PendingAttestation"; then
echo "pending"
exit 1
else
echo "attested"
exit 0
fi

114
scripts/generate-proof.sh Executable file
View file

@ -0,0 +1,114 @@
#!/bin/bash
# generate-proof.sh - Generate OpenTimestamp proof for a git commit
# Usage: generate-proof.sh <commit-hash> [output-file]
set -e
COMMIT_HASH="${1:-}"
OUTPUT_FILE="${2:-.ots/proof.ots}"
if [ -z "$COMMIT_HASH" ]; then
echo "Error: commit hash required" >&2
echo "Usage: $0 <commit-hash> [output-file]" >&2
exit 1
fi
# Ensure .ots directory exists
mkdir -p "$(dirname "$OUTPUT_FILE")"
# Function to generate proof using ots CLI
generate_with_ots_cli() {
local hash="$1"
local output="$2"
# Create a temporary file with the hash (convert hex to binary)
local temp_file=$(mktemp)
local temp_ots="${temp_file}.ots"
# Use python3 for hex to binary conversion (more portable than xxd)
python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$hash'))" > "$temp_file"
# Generate timestamp - ots creates .ots file alongside the original
ots stamp "$temp_file"
# Move the generated .ots file to the desired location
if [ -f "$temp_ots" ]; then
mkdir -p "$(dirname "$output")"
mv "$temp_ots" "$output"
echo "Generated proof with ots CLI: $output"
else
echo "Error: ots did not generate proof file" >&2
rm -f "$temp_file"
exit 1
fi
# Cleanup
rm -f "$temp_file"
}
# Function to generate proof using opentimestamps-nodejs
generate_with_node() {
local hash="$1"
local output="$2"
# Use nodejs opentimestamps library
# This requires @opentimestamps/ots to be installed globally
node -e "
const ots = require('@opentimestamps/ots');
const fs = require('fs');
const crypto = require('crypto');
const hash = '$hash';
const output = '$output';
// Convert hex hash to buffer
const hashBuffer = Buffer.from(hash, 'hex');
// Create timestamp
ots.Timestamp.hash(hashBuffer).then(timestamp => {
// In a real implementation, this would contact the calendar server
// For now, we create a minimal proof structure
console.log('Warning: nodejs fallback creates local proof only');
console.log('Install ots CLI for full functionality: https://github.com/opentimestamps/opentimestamps-client');
// Create a simple proof file (this is a placeholder - real impl needs calendar)
const proof = {
hash: hash,
timestamp: new Date().toISOString(),
status: 'pending'
};
fs.writeFileSync(output, JSON.stringify(proof, null, 2));
console.log('Generated local proof: ' + output);
}).catch(err => {
console.error('Error generating timestamp:', err);
process.exit(1);
});
"
}
# Check if ots CLI is available
if command -v ots &> /dev/null; then
echo "Using ots CLI..."
generate_with_ots_cli "$COMMIT_HASH" "$OUTPUT_FILE"
elif command -v node &> /dev/null && node -e "require('@opentimestamps/ots')" &> /dev/null; then
echo "Using opentimestamps-nodejs fallback..."
generate_with_node "$COMMIT_HASH" "$OUTPUT_FILE"
else
echo "Error: Neither ots CLI nor @opentimestamps/ots node package found" >&2
echo "Install one of:" >&2
echo " - ots CLI: https://github.com/opentimestamps/opentimestamps-client" >&2
echo " - Node package: npm install -g @opentimestamps/ots" >&2
exit 1
fi
# Get previous commit hash for chaining (before appending to proof file)
PREV_COMMIT=$(git rev-parse HEAD^1 2>/dev/null || echo "")
# Store previous commit hash in a separate metadata file for chaining
if [ -n "$PREV_COMMIT" ]; then
echo "$PREV_COMMIT" > "$(dirname "$OUTPUT_FILE")/prev-commit.txt"
echo "Chaining info saved: $PREV_COMMIT"
fi
echo "Proof generated for commit: $COMMIT_HASH"

98
scripts/install-ots-hook.sh Executable file
View file

@ -0,0 +1,98 @@
#!/bin/bash
# install-ots-hook.sh - Install OpenTimestamp post-commit hook
# Usage: install-ots-hook.sh [repository-path] [backfill]
set -e
REPO_PATH="${1:-.}"
ENABLE_BACKFILL="${2:-}" # Second arg: "backfill" to enable pre-commit backfill hook
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_PATH"
# Verify we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: not a git repository" >&2
exit 1
fi
HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
POST_COMMIT_HOOK="$HOOKS_DIR/post-commit"
echo "Installing OpenTimestamp hooks in: $REPO_PATH"
# Create the post-commit hook
cat > "$POST_COMMIT_HOOK" << 'HOOK_SCRIPT'
#!/bin/bash
# Post-commit hook: generate OpenTimestamp proof for each commit
set -e
# Get the commit hash
COMMIT_HASH=$(git rev-parse HEAD)
# Find the generate-proof.sh script
# Try relative paths first, then check common locations
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GENERATE_SCRIPT=""
# Check if generate-proof.sh is in the same directory as this hook
if [ -f "$SCRIPT_DIR/generate-proof.sh" ]; then
GENERATE_SCRIPT="$SCRIPT_DIR/generate-proof.sh"
# Check in .git/hooks directory
elif [ -f ".git/hooks/generate-proof.sh" ]; then
GENERATE_SCRIPT=".git/hooks/generate-proof.sh"
# Check in repository root
elif [ -f "generate-proof.sh" ]; then
GENERATE_SCRIPT="./generate-proof.sh"
# Check if skill is installed globally (common OpenClaw path)
elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" ]; then
GENERATE_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh"
else
echo "Warning: generate-proof.sh not found, skipping OpenTimestamp proof" >&2
exit 0
fi
# Make sure the script is executable
chmod +x "$GENERATE_SCRIPT"
# Generate the proof
echo "[ots] Generating proof for commit: $COMMIT_HASH"
"$GENERATE_SCRIPT" "$COMMIT_HASH"
echo "[ots] Proof generated successfully"
HOOK_SCRIPT
# Make the hook executable
chmod +x "$POST_COMMIT_HOOK"
echo "✓ Post-commit hook installed successfully"
# Optionally install pre-commit backfill hook
if [ "$ENABLE_BACKFILL" = "backfill" ]; then
PRE_COMMIT_HOOK="$HOOKS_DIR/pre-commit"
cp "$SCRIPT_DIR/pre-commit-backfill" "$PRE_COMMIT_HOOK"
chmod +x "$PRE_COMMIT_HOOK"
echo "✓ Pre-commit backfill hook installed (will backfill proofs before each commit)"
fi
# Optionally setup .gitignore
if [ -f "$SCRIPT_DIR/setup-gitignore.sh" ]; then
"$SCRIPT_DIR/setup-gitignore.sh" "$REPO_PATH"
elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/setup-gitignore.sh" ]; then
"$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/setup-gitignore.sh" "$REPO_PATH"
fi
echo ""
echo "Next steps:"
echo "1. Ensure scripts are accessible (copy to repo root or install skill globally)"
echo "2. Verify ots CLI or @opentimestamps/ots is installed"
echo "3. Add .ots/ to git tracking: git add .ots/"
echo "4. Commit proofs: git commit -m 'Add OpenTimestamp proofs'"
echo "5. Make a test commit to verify the hooks work"
echo ""
echo "To uninstall:"
echo " rm $POST_COMMIT_HOOK"
if [ "$ENABLE_BACKFILL" = "backfill" ]; then
echo " rm $PRE_COMMIT_HOOK"
fi

32
scripts/pre-commit-backfill Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Pre-commit hook: backfill proofs for all commits before creating new commit
# This ensures all historical commits have attested proofs
set -e
# Get the directory where this hook is installed
HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Find the backfill-proofs.sh script
BACKFILL_SCRIPT=""
if [ -f "$SCRIPT_DIR/backfill-proofs.sh" ]; then
BACKFILL_SCRIPT="$SCRIPT_DIR/backfill-proofs.sh"
elif [ -f ".git/hooks/backfill-proofs.sh" ]; then
BACKFILL_SCRIPT=".git/hooks/backfill-proofs.sh"
elif [ -f "backfill-proofs.sh" ]; then
BACKFILL_SCRIPT="./backfill-proofs.sh"
elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/backfill-proofs.sh" ]; then
BACKFILL_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/backfill-proofs.sh"
else
# Silently skip if backfill script not found
exit 0
fi
chmod +x "$BACKFILL_SCRIPT"
echo "[ots] Backfilling proofs before commit..."
"$BACKFILL_SCRIPT" .
echo "[ots] Ready to commit"

41
scripts/setup-gitignore.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
# setup-gitignore.sh - Create/update .gitignore for OpenTimestamp proofs
# Usage: setup-gitignore.sh [repository-path]
set -e
REPO_PATH="${1:-.}"
cd "$REPO_PATH"
# Verify we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: not a git repository" >&2
exit 1
fi
GITIGNORE=".gitignore"
# Create .gitignore if it doesn't exist
if [ ! -f "$GITIGNORE" ]; then
echo "# OpenTimestamp proofs cache (exclude from version control)" > "$GITIGNORE"
echo ".ots/.attestation-cache" >> "$GITIGNORE"
echo ""
echo "Created $GITIGNORE with OpenTimestamp cache exclusion"
else
# Check if already has the cache exclusion
if grep -q ".ots/.attestation-cache" "$GITIGNORE"; then
echo "$GITIGNORE already excludes .ots/.attestation-cache"
else
echo "" >> "$GITIGNORE"
echo "# OpenTimestamp proofs cache (exclude from version control)" >> "$GITIGNORE"
echo ".ots/.attestation-cache" >> "$GITIGNORE"
echo "Added .ots/.attestation-cache to $GITIGNORE"
fi
fi
echo ""
echo "Next steps:"
echo "1. Review and commit your .gitignore"
echo "2. Add .ots/ directory to track proofs: git add .ots/"
echo "3. Commit proofs: git commit -m 'Add OpenTimestamp proofs'"