#!/bin/sh set -eu log() { printf '%s\n' "[workspace-spaces-sync] $*" } fail() { log "ERROR: $*" >&2 exit 1 } DATA_ROOT="${DATA_ROOT:-/data}" S3CMD_BIN="${S3CMD_BIN:-s3cmd}" SPACES_BUCKET="${SPACES_BUCKET-}" SPACES_ENDPOINT="${SPACES_ENDPOINT-}" SPACES_SYNC_INTERVAL_SECONDS="${SPACES_SYNC_INTERVAL_SECONDS:-60}" # An unset bucket disables synchronization on platforms that provide their own # persistent volume, including HF Spaces and suitably configured Cloud Run. if [ -z "${SPACES_BUCKET}" ]; then exit 0 fi [ -n "${SPACES_ENDPOINT}" ] || fail "SPACES_ENDPOINT is required" [ -n "${AWS_ACCESS_KEY_ID-}" ] || fail "AWS_ACCESS_KEY_ID is required" [ -n "${AWS_SECRET_ACCESS_KEY-}" ] || fail "AWS_SECRET_ACCESS_KEY is required" command -v "${S3CMD_BIN}" >/dev/null 2>&1 || fail "s3cmd is not installed" endpoint="${SPACES_ENDPOINT#https://}" endpoint="${endpoint#http://}" endpoint="${endpoint%/}" case "${endpoint}" in ''|*[!A-Za-z0-9.-]*) fail "SPACES_ENDPOINT must be a regional hostname" ;; esac case "${SPACES_BUCKET}" in *[!a-z0-9.-]*|'') fail "SPACES_BUCKET contains unsupported characters" ;; esac case "${SPACES_SYNC_INTERVAL_SECONDS}" in ''|*[!0-9]*) fail "SPACES_SYNC_INTERVAL_SECONDS must be an integer" ;; esac if [ "${SPACES_SYNC_INTERVAL_SECONDS}" -lt 30 ]; then fail "SPACES_SYNC_INTERVAL_SECONDS must be at least 30" fi umask 077 config_file="$(mktemp /tmp/workspace-spaces-config.XXXXXX)" temporary_file='' cleanup() { rm -f "${config_file}" if [ -n "${temporary_file}" ]; then rm -f "${temporary_file}" fi } trap cleanup EXIT { printf '%s\n' '[default]' printf '%s\n' 'access_key =' printf '%s\n' 'secret_key =' printf '%s\n' 'bucket_location = US' printf '%s\n' "host_base = ${endpoint}" printf '%s\n' "host_bucket = %(bucket)s.${endpoint}" printf '%s\n' 'use_https = True' printf '%s\n' 'check_ssl_certificate = True' printf '%s\n' 'check_ssl_hostname = True' } > "${config_file}" s3cmd_run() { "${S3CMD_BIN}" --config="${config_file}" --no-progress "$@" } workspace_dir="${DATA_ROOT}/workspace" claude_dir="${DATA_ROOT}/claude" remote_uri="s3://${SPACES_BUCKET}/latest.tar.gz" restore_snapshot() { listing="$(s3cmd_run ls "${remote_uri}")" \ || fail "Unable to check ${remote_uri}" if [ -z "${listing}" ]; then log "No existing snapshot found in ${SPACES_BUCKET}" return 0 fi temporary_file="$(mktemp /tmp/workspace-restore.XXXXXX)" s3cmd_run get --force "${remote_uri}" "${temporary_file}" \ || fail "Unable to download ${remote_uri}" tar -tzf "${temporary_file}" >/dev/null \ || fail "Downloaded workspace snapshot is not a valid gzip archive" invalid_member="$( tar -tzf "${temporary_file}" \ | awk ' /(^|\/)\.\.($|\/)/ { print exit } $0 == "workspace" || $0 ~ /^workspace\// { next } $0 == "claude/projects" || $0 ~ /^claude\/projects\// { next } $0 == "claude/history.jsonl" { next } { print exit } ' )" [ -z "${invalid_member}" ] \ || fail "Workspace snapshot contains an invalid path: ${invalid_member}" tar --touch -C "${DATA_ROOT}" -xzf "${temporary_file}" rm -f "${temporary_file}" temporary_file='' log "Restored candidate snapshot from ${SPACES_BUCKET}" } upload_snapshot() { [ -d "${workspace_dir}" ] || return 0 temporary_file="$(mktemp /tmp/workspace-snapshot.XXXXXX)" # Preserve the candidate's code and the minimum Claude Code state needed to # inspect/resume conversations. Do not archive Claude credentials, settings, # plugins, caches, or other reusable user configuration. set -- workspace if [ -d "${claude_dir}/projects" ]; then set -- "$@" claude/projects fi if [ -f "${claude_dir}/history.jsonl" ]; then set -- "$@" claude/history.jsonl fi if ! tar -C "${DATA_ROOT}" -czf "${temporary_file}" \ --exclude='workspace/.venv' \ --exclude='workspace/__pycache__' \ --exclude='workspace/.pytest_cache' \ --exclude='workspace/.mypy_cache' \ --exclude='workspace/.ruff_cache' \ --exclude='workspace/.env' \ --exclude='workspace/.env.*' \ --exclude='workspace/.claude' \ --exclude='workspace/.claude.json' \ --exclude='workspace/.openrouter' \ --exclude='workspace/openrouter.env' \ --exclude='workspace/.git/index.lock' \ "$@" then log "WARNING: Workspace changed during snapshot; retrying later" >&2 rm -f "${temporary_file}" temporary_file='' return 1 fi if ! s3cmd_run put "${temporary_file}" "${remote_uri}"; then log "WARNING: Unable to upload workspace snapshot; retrying later" >&2 rm -f "${temporary_file}" temporary_file='' return 1 fi rm -f "${temporary_file}" temporary_file='' log "Uploaded candidate workspace and Claude transcript snapshot to ${SPACES_BUCKET}" } watch_snapshots() { stop_requested=0 trap 'stop_requested=1' INT TERM HUP while [ "${stop_requested}" -eq 0 ]; do upload_snapshot || true if [ "${stop_requested}" -eq 0 ]; then sleep "${SPACES_SYNC_INTERVAL_SECONDS}" & wait "$!" || true fi done log "Shutdown requested; uploading final workspace snapshot" upload_snapshot || true } case "${1:-}" in validate) log "Spaces synchronization configured for candidate bucket ${SPACES_BUCKET}" ;; restore) restore_snapshot ;; snapshot) upload_snapshot ;; watch) watch_snapshots ;; *) fail "Usage: $0 {validate|restore|snapshot|watch}" ;; esac