← Migration Readiness ReportReadinessRunbookRestore script

Paperclip Cutover Runbook

Phase 5 — Documentation only. This runbook is a controlled, reversible procedure. Nothing in this document is to be executed during authoring. All steps are gated behind the Go/No-Go gate (see below) and require recorded sign-offs. The legacy host 44.205.14.86 (".86") stays live throughout to guarantee instant rollback.


0. Overview & Topology

Item Value
Target host (new) 98.84.88.134 ("the .134")
Legacy host (rollback) 44.205.14.86 (".86") — keep live
Cutover DNS record paperclip.searskairos.ai (A-record)
Cutover window TBD — fill in at execution time
Runbook owner Srini
Rollback authority Srini (may be invoked unilaterally)

Required sign-offs (all three required before Go)

Area Sign-off owner Notes
AskPulse Ankam Confirms AskPulse agent secrets + routines ready
Infrastructure Rajesh Confirms host, DNS, ngrok, backup target
Executive Eddie Via Srini ONLY. Never message Eddie directly. Srini relays and records Eddie's approval.

1. Pre-Cutover Checklist

Complete and check off all items before approaching the Go/No-Go gate. Do not flip anything here — these are read-only verifications and confirmations.


2. Go / No-Go Gate

Do not proceed past this gate unless ALL of the following are TRUE. Any single NO = No-Go, abort, remain on .86.

# Gate condition Go?
G1 Pre-Cutover Checklist 100% complete
G2 Ankam (AskPulse) sign-off recorded
G3 Rajesh (infra) sign-off recorded
G4 Eddie (exec) approval relayed by Srini and recorded
G5 .86 verified live and healthy (rollback path proven)
G6 DNS TTL confirmed low (fast rollback possible)
G7 Fresh data dump checksum verified
G8 Rollback owner (Srini) present and acknowledged

Gate decision: ☐ GO ☐ NO-GO Recorded by: __________ Time: __________

If NO-GO at any point: stop, do not flip remaining steps, execute the Full Rollback for any steps already applied, and remain on .86.


3. Step 1 — Repoint DNS A-record

Point paperclip.searskairos.ai98.84.88.134.

Pre-check — capture and confirm current record (still pointing at .86):

dig +short paperclip.searskairos.ai A
# EXPECT: 44.205.14.86
dig +short paperclip.searskairos.ai A | tee /tmp/paperclip_dns_before.txt

Command — set the A-record to the new host (example uses Route 53; adapt to your DNS provider):

cat > /tmp/r53-cutover.json <<'JSON'
{
  "Comment": "Paperclip cutover: point to .134",
  "Changes": [{
    "Action": "UPSERT",
    "ResourceRecordSet": {
      "Name": "paperclip.searskairos.ai.",
      "Type": "A",
      "TTL": 60,
      "ResourceRecords": [{ "Value": "98.84.88.134" }]
    }
  }]
}
JSON
aws route53 change-resource-record-sets \
  --hosted-zone-id "$PAPERCLIP_ZONE_ID" \
  --change-batch file:///tmp/r53-cutover.json

Post-verify — confirm propagation to the new host:

dig +short paperclip.searskairos.ai A @1.1.1.1
# EXPECT: 98.84.88.134
curl -sS -o /dev/null -w '%{http_code}\n' https://paperclip.searskairos.ai/healthz
# EXPECT: 200

ROLLBACK — repoint DNS back to .86:

cat > /tmp/r53-rollback.json <<'JSON'
{
  "Comment": "Paperclip ROLLBACK: point to .86",
  "Changes": [{
    "Action": "UPSERT",
    "ResourceRecordSet": {
      "Name": "paperclip.searskairos.ai.",
      "Type": "A",
      "TTL": 60,
      "ResourceRecords": [{ "Value": "44.205.14.86" }]
    }
  }]
}
JSON
aws route53 change-resource-record-sets \
  --hosted-zone-id "$PAPERCLIP_ZONE_ID" \
  --change-batch file:///tmp/r53-rollback.json
dig +short paperclip.searskairos.ai A @1.1.1.1   # EXPECT: 44.205.14.86

4. Step 2 — Fresh data re-restore, THEN re-run bridge-views.sql

Order is mandatory: restore first, bridge-views second. Re-running bridge-views before the restore completes will bind views to stale/partial tables.

Pre-check — verify dump integrity and snapshot current DB for rollback:

# Verify the fresh dump checksum matches the recorded value
sha256sum -c /opt/paperclip/data/fresh_dump.sql.gz.sha256
# EXPECT: fresh_dump.sql.gz: OK

# Take a rollback snapshot of the CURRENT .134 database BEFORE touching it
pg_dump -Fc -h localhost -U paperclip paperclip \
  > /opt/paperclip/data/rollback_predump_$(date +%Y%m%d_%H%M%S).dump
ls -lh /opt/paperclip/data/rollback_predump_*.dump   # EXPECT: non-zero file

Command — restore fresh data, THEN run bridge-views:

# 1) Restore fresh data
gunzip -c /opt/paperclip/data/fresh_dump.sql.gz \
  | psql -h localhost -U paperclip -d paperclip -v ON_ERROR_STOP=1

# 2) ONLY after restore succeeds, run bridge-views
psql -h localhost -U paperclip -d paperclip -v ON_ERROR_STOP=1 \
  -f /opt/paperclip/sql/bridge-views.sql

Post-verify — confirm data freshness and views resolve:

psql -h localhost -U paperclip -d paperclip -At \
  -c "SELECT max(ingested_at) FROM source_events;"
# EXPECT: timestamp within the fresh dump window

psql -h localhost -U paperclip -d paperclip -At \
  -c "SELECT count(*) FROM bridge_active_view;"
# EXPECT: non-zero, matches expected row count

ROLLBACK — restore the pre-cutover .134 snapshot (drops fresh data + bridge-views):

psql -h localhost -U paperclip -d postgres -v ON_ERROR_STOP=1 -c \
  "DROP DATABASE paperclip; CREATE DATABASE paperclip OWNER paperclip;"
pg_restore -h localhost -U paperclip -d paperclip \
  /opt/paperclip/data/rollback_predump_<TIMESTAMP>.dump
# (If staying on .86 entirely, this DB rollback is optional — .86 is authoritative.)

5. Step 3 — Enable email + restore notification recipients

Set PAPERCLIP_EMAIL_ENABLED=true and restore SHSAI_ACTIVITY_DIGEST_TO and DTCC_NOTIFICATION_ALLOWLIST.

Pre-check — confirm email currently disabled and recipients currently empty/safe:

grep -E 'PAPERCLIP_EMAIL_ENABLED|SHSAI_ACTIVITY_DIGEST_TO|DTCC_NOTIFICATION_ALLOWLIST' \
  /opt/paperclip/.env
# EXPECT: PAPERCLIP_EMAIL_ENABLED=false ; the two recipient vars empty or unset

# Confirm backup of .env exists (from checklist)
ls -l /opt/paperclip/.env.prebrief.bak   # EXPECT: present

Command — write the values (staged from vault), then restart the service:

# Re-snapshot .env immediately before edit
cp -a /opt/paperclip/.env /opt/paperclip/.env.step3.bak

# Apply (values pulled from vault into shell env beforehand; not hardcoded here)
sed -i 's/^PAPERCLIP_EMAIL_ENABLED=.*/PAPERCLIP_EMAIL_ENABLED=true/' /opt/paperclip/.env
sed -i "s|^SHSAI_ACTIVITY_DIGEST_TO=.*|SHSAI_ACTIVITY_DIGEST_TO=${SHSAI_ACTIVITY_DIGEST_TO}|" /opt/paperclip/.env
sed -i "s|^DTCC_NOTIFICATION_ALLOWLIST=.*|DTCC_NOTIFICATION_ALLOWLIST=${DTCC_NOTIFICATION_ALLOWLIST}|" /opt/paperclip/.env

systemctl restart paperclip.service

Post-verify — confirm values loaded and a test send is gated to allowlist only:

systemctl show paperclip.service -p ActiveState   # EXPECT: ActiveState=active
grep PAPERCLIP_EMAIL_ENABLED /opt/paperclip/.env   # EXPECT: true
# Send a single canary to an internal allowlisted address only:
curl -sS -X POST https://paperclip.searskairos.ai/internal/email-canary \
  -H 'X-Cutover: 1'   # EXPECT: 202 Accepted; canary received internally

ROLLBACK — disable email and clear recipients (restore the backup):

cp -a /opt/paperclip/.env.step3.bak /opt/paperclip/.env
# or, surgically:
sed -i 's/^PAPERCLIP_EMAIL_ENABLED=.*/PAPERCLIP_EMAIL_ENABLED=false/' /opt/paperclip/.env
sed -i 's/^SHSAI_ACTIVITY_DIGEST_TO=.*/SHSAI_ACTIVITY_DIGEST_TO=/' /opt/paperclip/.env
sed -i 's/^DTCC_NOTIFICATION_ALLOWLIST=.*/DTCC_NOTIFICATION_ALLOWLIST=/' /opt/paperclip/.env
systemctl restart paperclip.service
grep PAPERCLIP_EMAIL_ENABLED /opt/paperclip/.env   # EXPECT: false

6. Step 4 — Enable ngrok tunnel

Enable and start ngrok-paperclip.service.

Pre-check — confirm unit is installed and currently disabled/stopped:

systemctl is-enabled ngrok-paperclip.service   # EXPECT: disabled
systemctl is-active  ngrok-paperclip.service   # EXPECT: inactive

Command:

systemctl enable --now ngrok-paperclip.service

Post-verify — confirm tunnel is up and reachable:

systemctl is-active ngrok-paperclip.service    # EXPECT: active
curl -sS http://127.0.0.1:4040/api/tunnels | jq -r '.tunnels[].public_url'
# EXPECT: an https public_url present

ROLLBACK — stop and disable the tunnel:

systemctl disable --now ngrok-paperclip.service
systemctl is-active ngrok-paperclip.service    # EXPECT: inactive

7. Step 5 — Bind agent secrets (selective), un-pause routines (selective), re-enable heartbeats

Selective = only the approved subset from the checklist (Ankam-approved). Do not bulk-enable.

Pre-check — confirm current paused/unbound state and capture inventory for rollback:

# Record which secrets are currently bound (for rollback diff)
paperclip-cli secrets list --bound > /tmp/secrets_bound_before.txt
# Record current routine states
paperclip-cli routines list --status > /tmp/routines_status_before.txt
# Record heartbeat state
paperclip-cli heartbeats status > /tmp/heartbeats_before.txt

Command — apply the approved selective lists only:

# Bind ONLY the approved subset (example names — use the Ankam-approved list)
for s in $(cat /opt/paperclip/cutover/approved_secrets.txt); do
  paperclip-cli secrets bind "$s"
done

# Un-pause ONLY the approved routines
for r in $(cat /opt/paperclip/cutover/approved_routines.txt); do
  paperclip-cli routines resume "$r"
done

# Re-enable heartbeats
paperclip-cli heartbeats enable --all

Post-verify:

paperclip-cli secrets list --bound        # EXPECT: approved subset now bound
paperclip-cli routines list --status      # EXPECT: approved subset = running
paperclip-cli heartbeats status           # EXPECT: heartbeats reporting healthy

ROLLBACK — re-pause routines, unbind the secrets just bound, disable heartbeats:

# Re-pause the routines we resumed
for r in $(cat /opt/paperclip/cutover/approved_routines.txt); do
  paperclip-cli routines pause "$r"
done
# Unbind the secrets we bound
for s in $(cat /opt/paperclip/cutover/approved_secrets.txt); do
  paperclip-cli secrets unbind "$s"
done
# Disable heartbeats
paperclip-cli heartbeats disable --all

8. Step 6 — Uncomment crons, point backup target

Pre-check — back up crontab and current backup config; confirm crons are still commented:

crontab -l > /tmp/crontab_before.txt
grep -n '^#' /tmp/crontab_before.txt        # EXPECT: paperclip cron lines commented
cp -a /opt/paperclip/backup.conf /opt/paperclip/backup.conf.bak
grep BACKUP_TARGET /opt/paperclip/backup.conf   # EXPECT: old target (record it)

Command — uncomment paperclip crons and repoint backup target:

# Uncomment only the paperclip-managed cron lines
sed -i '/# >>> paperclip >>>/,/# <<< paperclip <<</ s/^#\( \)\?//' \
  <(crontab -l) > /tmp/crontab_new.txt
crontab /tmp/crontab_new.txt

# Point backup at the new target
sed -i "s|^BACKUP_TARGET=.*|BACKUP_TARGET=${NEW_BACKUP_TARGET}|" /opt/paperclip/backup.conf

Post-verify:

crontab -l | grep -A20 '# >>> paperclip >>>'   # EXPECT: lines now active (uncommented)
grep BACKUP_TARGET /opt/paperclip/backup.conf  # EXPECT: new target
# Dry-run a backup to confirm target is writable
paperclip-cli backup run --dry-run             # EXPECT: success, writes to new target

ROLLBACK — recomment crons and restore old backup target:

crontab /tmp/crontab_before.txt                # restores commented crons
cp -a /opt/paperclip/backup.conf.bak /opt/paperclip/backup.conf
grep BACKUP_TARGET /opt/paperclip/backup.conf  # EXPECT: old target restored

9. Step 7 — Keep .86 live for rollback

.86 is not decommissioned at cutover. It remains live, serving, and authoritative as the rollback target until cutover is declared stable (post-soak).

Pre-check / standing verification.86 healthy throughout the window:

ssh ops@44.205.14.86 'uptime && systemctl is-active paperclip.service'
# EXPECT: active
curl -sS -o /dev/null -w '%{http_code}\n' --resolve paperclip.searskairos.ai:443:44.205.14.86 \
  https://paperclip.searskairos.ai/healthz
# EXPECT: 200 (proves .86 still serves the app directly)

Command — none. Explicitly do nothing to .86:

# INTENTIONALLY NO-OP. Do not stop, disable, or wipe .86.
echo "Leaving .86 (44.205.14.86) live for rollback. No action taken."

Post-verify.86 still ready to take traffic on rollback:

curl -sS -o /dev/null -w '%{http_code}\n' --resolve paperclip.searskairos.ai:443:44.205.14.86 \
  https://paperclip.searskairos.ai/healthz   # EXPECT: 200

ROLLBACK.86 IS the rollback. Repoint DNS to it (see Full Rollback). Decommission only after a clean soak and explicit sign-off (out of scope for this runbook).


10. Full Rollback

Invoke at any point on No-Go, failed post-verify, or incident. Rollback authority: Srini (may act unilaterally). Execute in this order (reverse of cutover); skip steps not yet applied.

  1. Repoint DNS back to .86 (highest priority — restores user-facing traffic):
    aws route53 change-resource-record-sets \
      --hosted-zone-id "$PAPERCLIP_ZONE_ID" \
      --change-batch file:///tmp/r53-rollback.json
    dig +short paperclip.searskairos.ai A @1.1.1.1   # EXPECT: 44.205.14.86
    
  2. Disable email + clear recipients (prevents the new host from sending):
    sed -i 's/^PAPERCLIP_EMAIL_ENABLED=.*/PAPERCLIP_EMAIL_ENABLED=false/' /opt/paperclip/.env
    sed -i 's/^SHSAI_ACTIVITY_DIGEST_TO=.*/SHSAI_ACTIVITY_DIGEST_TO=/' /opt/paperclip/.env
    sed -i 's/^DTCC_NOTIFICATION_ALLOWLIST=.*/DTCC_NOTIFICATION_ALLOWLIST=/' /opt/paperclip/.env
    systemctl restart paperclip.service
    
  3. Disable ngrok tunnel:
    systemctl disable --now ngrok-paperclip.service
    
  4. Re-pause routines + unbind secrets + disable heartbeats (selective, reverse of Step 5):
    for r in $(cat /opt/paperclip/cutover/approved_routines.txt); do paperclip-cli routines pause "$r"; done
    for s in $(cat /opt/paperclip/cutover/approved_secrets.txt); do paperclip-cli secrets unbind "$s"; done
    paperclip-cli heartbeats disable --all
    
  5. Recomment crons + restore old backup target:
    crontab /tmp/crontab_before.txt
    cp -a /opt/paperclip/backup.conf.bak /opt/paperclip/backup.conf
    
  6. (Optional) Restore .134 DB snapshot — only if .134 must be reverted; .86 is authoritative so this is usually unnecessary:
    psql -h localhost -U paperclip -d postgres -c \
      "DROP DATABASE paperclip; CREATE DATABASE paperclip OWNER paperclip;"
    pg_restore -h localhost -U paperclip -d paperclip \
      /opt/paperclip/data/rollback_predump_<TIMESTAMP>.dump
    
  7. Confirm .86 serving and healthy:
    curl -sS -o /dev/null -w '%{http_code}\n' --resolve paperclip.searskairos.ai:443:44.205.14.86 \
      https://paperclip.searskairos.ai/healthz   # EXPECT: 200
    

Post-rollback: notify Ankam and Rajesh directly; relay status to Eddie via Srini only. Record rollback time, trigger, and the last successfully applied step.


11. Sign-off Record

Role Name Approval (Y/N) Timestamp Notes
AskPulse Ankam
Infrastructure Rajesh
Executive (via Srini) Eddie Relayed by Srini — Eddie not contacted directly
Runbook owner / Rollback authority Srini

Reminder: Eddie is only reached through Srini. Do not message Eddie directly under any circumstance — Srini relays both the request for approval and any rollback notifications.