Compare commits

...
Author SHA1 Message Date
cayce 121183ac83 Merge PR #13: generación de backups cifrados
ci / lint-test-leaks (push) Canceled after 0s
2026-10-28 20:59:35 +00:00
cayce b87a51afc0 backup: encrypted daily backups — VPS generates, Mac pulls (Tanda B, point 1)
ci / lint-test-leaks (pull_request) Canceled after 0s
deploy/backup/make-backup.sh (installed as /usr/local/bin/cayce-make-backup):
- pg_dump custom-format of the cayce-ops DB (URL from env or .env,
  SQLAlchemy driver qualifier stripped, credentials never logged)
- gitea dump (repos + sqlite + attachments), live; consistency notes in RESTORE.md
- config tar: Caddyfile(+d), gitea app.ini + internal_token, cayce-* units, .env
- EACH artifact encrypted with age BEFORE leaving the tmpdir; recipient
  read from /etc/cayce-backup/age-recipient.pub (public key only). The
  private age key never touches this server.
- plaintext exists ONLY in a mktemp dir removed by an EXIT trap; a partial
  day directory is removed on failure; any step failure => exit != 0 =>
  journald entry
- manifest.sha256 over the encrypted artifacts (pull side verifies
  integrity WITHOUT the age private key)
- destination /var/backups/cayce/YYYY-MM-DD/ (750 root:backup-pull, 640)
- retention: 7 daily dirs, pruned by mtime

systemd: cayce-backup.service (oneshot, hardened: ProtectSystem=strict,
NoNewPrivileges, ProtectHome=read-only) + cayce-backup.timer (daily
03:15 UTC, Persistent). Timer ENABLED on the VPS per operator instruction;
runs fail loudly (exit 1, clear journald message) until the age recipient
is provided — that is the designed not-configured state.
2026-10-28 20:59:28 +00:00
cayce d527164434 Merge PR #12: identidad sin afirmación de jurisdicción
ci / lint-test-leaks (push) Canceled after 0s
2026-10-28 20:01:09 +00:00
cayce 1c4ba234d0 identity: remove unverified jurisdiction claim, state the approval principle
ci / lint-test-leaks (pull_request) Canceled after 0s
The previous text asserted participation in an external jurisdiction
pilot without a verifiable source. Replaced with what is actually true
of this codebase: every external-effect action requires cryptographically
signed human approval, with a key that never resides on Cayce
infrastructure (D16).
2026-10-28 20:01:02 +00:00
cayce 64f89df289 Merge PR #11: clasificador con negaciones en español
ci / lint-test-leaks (push) Canceled after 0s
2026-10-28 19:59:50 +00:00
4 changed files with 151 additions and 5 deletions
+8 -5
View File
@@ -1,10 +1,13 @@
# Cayce Ops
Cayce Ops is the autonomous operations stack of **Cayce** — an autonomous entity
operating under the Próspera ZEDE agentic-companies jurisdiction pilot. Cayce runs
[bitcoinhotels.org](https://bitcoinhotels.org) (Bitcoin-native travel guides) and
[hardcap.news](https://hardcap.news) (Bitcoin security intelligence). The
experiment is managed by Cayce itself.
CayceOps es el stack de operaciones de Cayce, un experimento de operaciones
agénticas de Cayce Co. Toda acción con efectos externos requiere aprobación
humana firmada criptográficamente, con una clave que nunca reside en la
infraestructura de Cayce.
Cayce operates [bitcoinhotels.org](https://bitcoinhotels.org)
(Bitcoin-native travel guides) and [hardcap.news](https://hardcap.news)
(Bitcoin security intelligence).
Five decoupled components share one Postgres database. Each is a CLI command,
driven by cron/systemd timers. Nothing runs without an audit trail.
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# cayce-backup — encrypted daily backup of CayceOps + Gitea + config.
#
# Design contract (Tanda B):
# - EVERY artifact is encrypted with age BEFORE it leaves the tmpdir.
# No plaintext is ever written outside the tmpdir, which is removed
# by a trap even on failure.
# - The age recipient lives in /etc/cayce-backup/age-recipient.pub
# (public key only). The PRIVATE key never touches this server:
# decryption happens on the operator's machine after a pull.
# - Any step failure => non-zero exit => journald entry (run via systemd).
# - Retention: 7 daily directories under /var/backups/cayce/.
#
# The restore procedure lives in deploy/backup/RESTORE.md.
set -euo pipefail
DATE="$(date -u +%F)"
BACKUP_ROOT="/var/backups/cayce"
DEST="$BACKUP_ROOT/$DATE"
RECIPIENT_FILE="/etc/cayce-backup/age-recipient.pub"
CAYCE_DIR="/home/ubuntu/cayce-ops"
ENV_FILE="$CAYCE_DIR/.env"
RETENTION_DAYS=7
log() { echo "[cayce-backup] $(date -u +%FT%TZ) $*"; }
die() { echo "[cayce-backup] FATAL: $*" >&2; exit 1; }
# --- preconditions -------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "must run as root (systemd)"
command -v age >/dev/null || die "age not installed"
[[ -r "$RECIPIENT_FILE" ]] || die "age recipient not configured: create $RECIPIENT_FILE (public key only)"
RECIPIENT="$(tr -d "[:space:]" < "$RECIPIENT_FILE")"
[[ "$RECIPIENT" == age1* ]] || die "$RECIPIENT_FILE does not look like an age1... public key"
# --- workspace: everything plaintext lives here and dies here ------------------
TMP="$(mktemp -d /tmp/cayce-backup.XXXXXX)"
# 711: root-only listing, but the git user must be able to traverse into
# its own 700 subdir for gitea dump; plaintext root files stay unreadable
chmod 711 "$TMP"
cleanup() {
rm -rf "$TMP"
if [[ "${FAILED:-0}" == "1" ]]; then
rm -rf "$DEST" # never leave a partial day directory
fi
}
trap cleanup EXIT
STAGE="$TMP/stage" # encrypted artifacts land here before install
install -d -m 700 "$STAGE"
encrypt() { # encrypt <plaintext-path> <name.age>
age -r "$RECIPIENT" -o "$STAGE/$2" "$1"
}
# --- 1. Postgres dump (cayce-ops DB) --------------------------------------------
DB_URL="${CAYCE_OPS_DATABASE_URL:-}"
if [[ -z "$DB_URL" && -r "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
DB_URL="$(grep -E "^CAYCE_OPS_DATABASE_URL=" "$ENV_FILE" | head -1 | cut -d= -f2-)"
fi
[[ -n "$DB_URL" ]] || die "no CAYCE_OPS_DATABASE_URL in env or $ENV_FILE"
PG_URL="$(printf "%s" "$DB_URL" | sed -E "s#postgresql\+[a-z]+://#postgresql://#")"
log "dumping database from ${PG_URL%%@*@}***@..."
pg_dump "$PG_URL" --format=custom --file "$TMP/cayce-ops-db.dump"
encrypt "$TMP/cayce-ops-db.dump" "cayce-ops-db.dump.age"
log "database dump: $(du -h "$TMP/cayce-ops-db.dump" | cut -f1) plaintext, encrypted ok"
# --- 2. Gitea dump (repos + sqlite + attachments) -------------------------------
GITEA_BIN="$(command -v gitea || echo /usr/local/bin/gitea)"
[[ -x "$GITEA_BIN" ]] || die "gitea binary not found"
install -d -o git -g git -m 700 "$TMP/gitea-work"
log "running gitea dump (live; see RESTORE.md for consistency notes)..."
sudo -u git "$GITEA_BIN" dump -c /etc/gitea/app.ini \
--file "$TMP/gitea-work/gitea-dump.zip" --tempdir "$TMP/gitea-work" --quiet
encrypt "$TMP/gitea-work/gitea-dump.zip" "gitea-dump.zip.age"
log "gitea dump: $(du -h "$TMP/gitea-work/gitea-dump.zip" | cut -f1) plaintext, encrypted ok"
# --- 3. Config tar (Caddy, Gitea app.ini + internal_token, units, .env) ----------
CONF="$TMP/config"
install -d -m 700 "$CONF"
mkdir -p "$CONF/caddy" "$CONF/gitea" "$CONF/systemd" "$CONF/cayce-ops"
cp -a /etc/caddy/Caddyfile "$CONF/caddy/" 2>/dev/null || true
cp -a /etc/caddy/Caddyfile.d "$CONF/caddy/" 2>/dev/null || true
cp -a /etc/gitea/app.ini "$CONF/gitea/" 2>/dev/null || true
cp -a /etc/gitea/internal_token "$CONF/gitea/" 2>/dev/null || true
find /etc/systemd/system -maxdepth 1 -name "cayce-*" \
-exec cp -a {} "$CONF/systemd/" \; 2>/dev/null || true
[[ -r "$ENV_FILE" ]] && cp -a "$ENV_FILE" "$CONF/cayce-ops/dot-env" \
|| log "WARNING: $ENV_FILE not present — config backup continues without it"
# at least Caddy or Gitea config must exist, else the tar is useless
[[ -e "$CONF/caddy/Caddyfile" || -e "$CONF/gitea/app.ini" ]] \
|| die "no config sources found (Caddyfile/app.ini missing)"
tar -C "$CONF" -czf "$TMP/config.tar.gz" .
encrypt "$TMP/config.tar.gz" "config.tar.gz.age"
log "config tar: $(du -h "$TMP/config.tar.gz" | cut -f1) plaintext, encrypted ok"
# --- 4. install + manifest -------------------------------------------------------
install -d -m 750 -o root -g backup-pull "$BACKUP_ROOT" 2>/dev/null \
|| install -d -m 700 "$BACKUP_ROOT" # backup-pull group may not exist yet
install -d -m 750 -o root -g backup-pull "$DEST" 2>/dev/null \
|| install -d -m 700 "$DEST"
install -m 640 -o root -g backup-pull "$STAGE"/*.age "$DEST/" 2>/dev/null \
|| install -m 600 "$STAGE"/*.age "$DEST/"
( cd "$DEST" && sha256sum ./*.age > "$TMP/manifest.sha256" )
install -m 640 -o root -g backup-pull "$TMP/manifest.sha256" "$DEST/manifest.sha256" 2>/dev/null \
|| install -m 600 "$TMP/manifest.sha256" "$DEST/manifest.sha256"
log "manifest written: $(wc -l < "$DEST/manifest.sha256") artifacts"
# --- 5. retention -----------------------------------------------------------------
PRUNED=0
while IFS= read -r old; do
rm -rf "$old"
PRUNED=$((PRUNED + 1))
log "retention: removed $old"
done < <(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -name "2*-*-*" -mtime +"$RETENTION_DAYS")
log "retention: kept last $RETENTION_DAYS days (pruned $PRUNED)"
log "BACKUP OK → $DEST ($(du -sh "$DEST" | cut -f1))"
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Cayce — encrypted daily backup (CayceOps DB + Gitea + config)
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cayce-make-backup
# hardening
NoNewPrivileges=true
ProtectHome=read-only
ReadWritePaths=/var/backups/cayce /tmp
ProtectSystem=strict
PrivateTmp=false
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Cayce — daily backup schedule (03:15 UTC)
[Timer]
OnCalendar=*-*-* 03:15:00 UTC
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target