Record repaired backup lane and stage bounded consumer refresh packet
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 42s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
tegwick 2026-10-03 21:58:57 +02:00
parent 42d71faec9
commit afa1058062
5 changed files with 322 additions and 1 deletions

View file

@ -0,0 +1,36 @@
{
"artifact": "root-custody-history-20261003T195656Z-a52ec01b2a4a4c28ba21a52eb29e7cb7.json.age",
"ciphertext_sha256": "577498be3c6566e9622fe0d87ea3ad55063941acfab3aeb0accfc4a05dd21200",
"coordinates_only_updated": true,
"current_live_baseline_matches": true,
"decrypted_history_matches": true,
"digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248",
"escrow_and_unrelated_fields_preserved": true,
"finished_at": "2026-10-03T19:56:59.023904+00:00",
"historical_config_present": true,
"historical_credential_status": "historical-owner-and-revocation-unverified",
"historical_share_matches_current_lane": false,
"history_entries": 1,
"independent_escrow_decryption_verified": true,
"independent_escrow_recipient_matches_current_lane": true,
"kv_version": 1,
"lane_cas_noop": false,
"lane_kv_version": 3,
"local_historical_rclone_used": false,
"ok": true,
"provider": "existing-governed-nextcloud-backup",
"provider_roundtrip_verified_before_cas": true,
"published_lane_kv_version": 4,
"readback_http_status": 200,
"record_id": "nk-custody:platform-root",
"recorded_at": "2026-10-03T19:56:48.310198+00:00",
"replacement_share_created": true,
"replacement_share_id": 23,
"revision": 1,
"runtime_consumers_refreshed": false,
"scope": "current-owner-read-and-governed-history-provider-roundtrip",
"stage": "complete",
"status": "verified",
"unknown_shares_and_old_backups_preserved": true,
"upload_http_status": 201
}

View file

@ -0,0 +1,30 @@
# Reviewed candidate backup client refresh
Prepared from railiance-platform source `f1a780d830a118eecdc0ada1885e7b8f95b51072`;
immutable Forgejo ConfigMap `backup-verified-debb9e4c59c9` and its existing mount
patch are review artifacts. This directory is excluded from ordinary GitOps
kustomization and has not been applied. It preserves existing env and other mounts.
Before runtime promotion:
1. Repair repo-manager historical `custodian-WP-*` identifier compatibility without
renumbering those owner records. Exact primary projection is currently unproved.
2. Synchronize approved railiance-platform source on railiance01, so CNPG's
existing hostPath receives the corrected public DAV header. This ConfigMap
updates Forgejo only; it does not update CNPG.
3. Confirm ESO's refreshed lane after KV4 publication, using safe metadata and
privately compared credential equality if needed, never printed Secret bodies.
4. Review the immutable bundle and scoped mount patch, then incorporate the new
ConfigMap and volume reference into the adopted `k8s/gitops` source. Use reviewed
ArgoCD sync rather than the old direct deployment patch helper. Keep the old
immutable bundle until no rollback ReplicaSet references it.
5. Verify worker recreation after source/ESO refresh, run
`python3 scripts/inspect_backup_delivery.py --platform-root ~/railiance-platform`,
and require both mounted source comparisons true. EnvFrom needs a new process;
ESO Ready alone is insufficient.
6. Obtain a separately reviewed existing bounded backup/provider receipt before
declaring activity consumer acceptance. Do not automatically fire schedules.
No image update, worker restart, schedule or provider mutation has occurred.
Current source-owned error-boundary hardening from T06 remains separate; this
packet does not claim it was loaded by the existing image.

View file

@ -0,0 +1,208 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: backup-verified-debb9e4c59c9
namespace: activity-core
labels:
railiance.io/workplan: RPF-WP-0029
immutable: true
data:
tools__cmd__forgejo-backup: "#!/usr/bin/env bash\n# tools/cmd/forgejo-backup \u2014\
\ Forgejo Option A backup (RAIL-HO-WP-0005 T04/T09)\n#\n# Daily: forgejo dump\
\ (blob state) + pg_dump (forgejo-db) \u2192 age \u2192 Nextcloud forgejo/\n#\
\ Retention: 14 daily + 4 weekly uploads (Sunday); local cache keep 7 per type.\n\
#\n# Env:\n# KUBECONFIG (default ~/.kube/config-hosteurope)\n\
# RAILIANCE_BACKUP_NC_TOKEN Nextcloud WebDAV token\n# RAILIANCE_BACKUP_DRY_RUN=1\
\ skip upload and remote side effects\nset -euo pipefail\n\n\nROOT=\"$(cd \"\
$(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\n# shellcheck source=lib/railiance-print.sh\n\
source \"${ROOT}/lib/railiance-print.sh\"\n# shellcheck source=lib/railiance-backup-common.sh\n\
source \"${ROOT}/lib/railiance-backup-common.sh\"\n\nKUBECONFIG=\"${KUBECONFIG:-${HOME}/.kube/config-hosteurope}\"\
\nexport KUBECONFIG\nDRY_RUN=\"${RAILIANCE_BACKUP_DRY_RUN:-0}\"\n\nFORGEJO_NAMESPACE=\"\
${FORGEJO_NAMESPACE:-forgejo}\"\nFORGEJO_RELEASE=\"${FORGEJO_RELEASE:-forgejo}\"\
\nFORGEJO_DB_NAMESPACE=\"${FORGEJO_DB_NAMESPACE:-databases}\"\nFORGEJO_DB_CLUSTER=\"\
${FORGEJO_DB_CLUSTER:-forgejo-db}\"\n\nkubectl_get_pod() {\n local ns=\"$1\"\
\ selector=\"$2\" attempt out\n for attempt in 1 2 3 4 5 6 7 8 9 10; do\n \
\ out=\"$(kubectl get pods -n \"${ns}\" -l \"${selector}\" \\\n -o jsonpath='{.items[0].metadata.name}'\
\ 2>/dev/null || true)\"\n if [[ -n \"${out}\" ]]; then\n echo \"${out}\"\
\n return 0\n fi\n sleep $((attempt * 3))\n done\n return 1\n}\n\n\
BACKUP_DIR=\"${XDG_CACHE_HOME:-$HOME/.cache}/railiance/backups/forgejo\"\nTS=\"\
$(date -u +%Y%m%dT%H%M%SZ)\"\nDOW=\"$(date -u +%u)\" # 7 = Sunday\n\nDUMP_PLAIN=\"\
${BACKUP_DIR}/forgejo-dump-${TS}.zip\"\nDUMP_AGE=\"${DUMP_PLAIN}.age\"\nDB_PLAIN=\"\
${BACKUP_DIR}/forgejo-db-${TS}.sql\"\nDB_AGE=\"${DB_PLAIN}.age\"\n\ncleanup_plain()\
\ {\n rm -f \"${DUMP_PLAIN}\" \"${DB_PLAIN}\"\n}\ntrap cleanup_plain EXIT\n\n\
if [[ \"${DRY_RUN}\" != \"1\" ]]; then\n railiance_backup_require_openbao_lane\n\
fi\nrailiance_backup_require_tools\n\nFORGEJO_DB_POD=\"${FORGEJO_DB_POD:-$(kubectl_get_pod\
\ \"${FORGEJO_DB_NAMESPACE}\" \\\n \"cnpg.io/cluster=${FORGEJO_DB_CLUSTER}\"\
\ || true)}\"\nif [[ -z \"${FORGEJO_DB_POD}\" ]]; then\n bad \"preflight\" \"\
forgejo-db pod not found (cluster ${FORGEJO_DB_CLUSTER})\"\n exit 1\nfi\n\nif\
\ ! kubectl wait --for=condition=Ready \"pod/${FORGEJO_DB_POD}\" \\\n -n \"${FORGEJO_DB_NAMESPACE}\"\
\ --timeout=180s >/dev/null 2>&1; then\n bad \"preflight\" \"forgejo-db pod not\
\ Ready (check: make forgejo-db-status)\"\n exit 1\nfi\n\nPROD_POD=\"$(kubectl_get_pod\
\ \"${FORGEJO_NAMESPACE}\" \\\n \"app.kubernetes.io/instance=${FORGEJO_RELEASE}\"\
\ || true)\"\nif [[ -z \"${PROD_POD}\" ]]; then\n bad \"preflight\" \"Forgejo\
\ app pod not found in ${FORGEJO_NAMESPACE}\"\n exit 1\nfi\n\nmkdir -p \"${BACKUP_DIR}\"\
\nprint_hdr \"Forgejo backup \u2014 ${TS}\"\nok \"kubeconfig\" \"${KUBECONFIG}\"\
\nok \"forgejo pod\" \"${FORGEJO_NAMESPACE}/${PROD_POD}\"\nok \"forgejo-db\" \"\
${FORGEJO_DB_NAMESPACE}/${FORGEJO_DB_POD}\"\n\n# 1. Application dump (repos, packages,\
\ attachments, LFS, avatars)\nok \"forgejo dump\" \"running in pod\u2026\"\npython3\
\ \"${ROOT}/scripts/capture_forgejo_archive.py\" \\\n --namespace \"${FORGEJO_NAMESPACE}\"\
\ --pod \"${PROD_POD}\" --output \"${DUMP_PLAIN}\"\nok \"forgejo dump\" \"$(du\
\ -h \"${DUMP_PLAIN}\" | awk '{print $1}') $(basename \"${DUMP_PLAIN}\")\"\n\n\
# 2. PostgreSQL logical dump (plain SQL to stdout; CNPG root FS is read-only)\n\
ok \"pg_dump\" \"database forgejo\u2026\"\nkubectl exec -n \"${FORGEJO_DB_NAMESPACE}\"\
\ \"${FORGEJO_DB_POD}\" -c postgres -- \\\n pg_dump -U postgres -d forgejo --no-owner\
\ --no-acl > \"${DB_PLAIN}\"\nok \"pg_dump\" \"$(du -h \"${DB_PLAIN}\" | awk '{print\
\ $1}') $(basename \"${DB_PLAIN}\")\"\n\n# 3. Encrypt\nrailiance_backup_encrypt\
\ \"${DUMP_PLAIN}\" \"${DUMP_AGE}\"\nrailiance_backup_encrypt \"${DB_PLAIN}\"\
\ \"${DB_AGE}\"\nok \"age\" \"encrypted dump + db\"\n\nif [[ \"${DRY_RUN}\" ==\
\ \"1\" ]]; then\n warn \"dry-run\" \"skipping Nextcloud upload\"\nelse\n railiance_backup_nc_upload\
\ \"${DUMP_AGE}\" \"forgejo-dump-${TS}.zip.age\"\n ok \"upload\" \"forgejo-dump-${TS}.zip.age\"\
\n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-${TS}.sql.age\"\n ok\
\ \"upload\" \"forgejo-db-${TS}.sql.age\"\n\n if [[ \"${DOW}\" == \"7\" ]]; then\n\
\ railiance_backup_nc_upload \"${DUMP_AGE}\" \"forgejo-dump-weekly-${TS}.zip.age\"\
\n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-weekly-${TS}.sql.age\"\
\n ok \"weekly\" \"Sunday copies uploaded\"\n fi\n\n railiance_backup_record_success\
\ \"${BACKUP_DIR}\" \"${TS}\"\nfi\n\n# 4. Local cache prune (keep 7 per type)\n\
railiance_backup_prune_local \"${BACKUP_DIR}\" 'forgejo-dump-*.zip.age' 7\nrailiance_backup_prune_local\
\ \"${BACKUP_DIR}\" 'forgejo-db-*.sql.age' 7\nrailiance_backup_prune_local \"\
${BACKUP_DIR}\" 'forgejo-dump-*.zip' 3\nrailiance_backup_prune_local \"${BACKUP_DIR}\"\
\ 'forgejo-db-*.sql' 3\nok \"cache\" \"local copies pruned\"\n\necho\nok \"done\"\
\ \"Forgejo backup complete \u2014 ${TS}\"\nif [[ \"${DRY_RUN}\" != \"1\" ]];\
\ then\n echo \" Dump: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-dump-${TS}.zip.age\"\
\n echo \" DB: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-db-${TS}.sql.age\"\
\n echo\n echo \" Decrypt with ~/.config/age/railiance-backup.key\"\n echo\
\ \" Restore drill: railiance-infra tools/forgejo-restore-drill.sh\"\nfi"
scripts__capture_forgejo_archive.py: "#!/usr/bin/env python3\n\"\"\"Capture only\
\ a completed, byte-identical and CRC-verified Forgejo ZIP.\"\"\"\nimport argparse\n\
import hashlib\nfrom pathlib import Path\nimport secrets\nimport subprocess\n\
import time\nimport zipfile\n\n\nESSENTIALS_FLAGS = ['--skip-package-data', '--skip-log',\
\ '--skip-index', '--skip-repo-archives']\n\n\ndef validate_archive(path, profile=\"\
full\"):\n with zipfile.ZipFile(path) as archive:\n if sum(i.file_size\
\ for i in archive.infolist()) > 20*1024**3:\n raise ValueError('archive\
\ exceeds recovery size bound')\n names = set(archive.namelist())\n \
\ if 'forgejo-db.sql' not in names or not any(n.startswith('repos/') for n\
\ in names):\n raise ValueError('required archive content missing')\n\
\ if profile == 'essentials' and any(n.startswith(('data/packages/', 'data/repo-archive/',\
\ 'data/indexers/')) and not n.endswith('/') for n in names):\n raise\
\ ValueError('excluded data present in essentials archive')\n if archive.testzip()\
\ is not None:\n raise ValueError('archive checksum failure')\n\n\n\
def capture(namespace, pod, destination, profile=\"full\"):\n if profile not\
\ in (\"full\", \"essentials\"): raise ValueError(\"unknown archive profile\"\
)\n k = ['kubectl', 'exec', '--request-timeout=120s', '-n', namespace, pod,\
\ '-c', 'gitea', '--']\n def call(args, timeout=150):\n r = subprocess.run(k\
\ + args, capture_output=True, timeout=timeout)\n if r.returncode: raise\
\ ValueError('capture command failed')\n return r.stdout\n remote =\
\ '/tmp/wp0029-backup-' + secrets.token_hex(12)\n completed = False\n try:\n\
\ # Completion belongs to this exact process, not a global pgrep or file\
\ existence.\n flags = ' '.join(ESSENTIALS_FLAGS) if profile == 'essentials'\
\ else ''\n command = f'umask 077; forgejo dump {flags} -f {remote}.zip\
\ >{remote}.log 2>&1; result=$?; printf \"%s\" \"$result\" >{remote}.exit'\n \
\ call(['sh','-c', 'nohup sh -c \"$1\" >/dev/null 2>&1 </dev/null &', 'sh',\
\ command])\n for _ in range(180):\n r = subprocess.run(k +\
\ ['cat',remote+'.exit'], capture_output=True, timeout=30)\n if r.returncode\
\ == 0:\n completed = True\n if r.stdout.strip()\
\ != b'0': raise ValueError('dump process failed')\n break\n \
\ time.sleep(10)\n else: raise ValueError('dump completion timeout')\n\
\ size = int(call(['stat','-c%s',remote+'.zip']).strip())\n expected\
\ = call(['sha256sum',remote+'.zip']).split()[0].decode()\n if size <=\
\ 0: raise ValueError('empty archive')\n chunk = 4*1024*1024\n with\
\ destination.open('xb') as out:\n destination.chmod(0o600)\n \
\ for index in range((size+chunk-1)//chunk):\n piece = call(['dd','if='+remote+'.zip','bs='+str(chunk),'skip='+str(index),'count=1'])\n\
\ if len(piece) != min(chunk,size-index*chunk): raise ValueError('short\
\ archive chunk')\n out.write(piece)\n with destination.open('rb')\
\ as source:\n actual = hashlib.file_digest(source,'sha256').hexdigest()\n\
\ if actual != expected: raise ValueError('archive transfer mismatch')\n\
\ validate_archive(destination, profile)\n if profile == \"essentials\"\
:\n from forgejo_essentials_profile import seal\n seal(destination)\n\
\ validate_archive(destination, profile)\n finally:\n # Do\
\ not remove a file while a timed-out producer may still be writing it.\n \
\ if completed:\n call(['rm','-f',remote+'.zip',remote+'.log',remote+'.exit'])\n\
\n\ndef main():\n p=argparse.ArgumentParser(description=__doc__)\n p.add_argument('--namespace',required=True);\
\ p.add_argument('--pod',required=True)\n p.add_argument('--output',required=True,type=Path)\n\
\ p.add_argument(\"--profile\",choices=[\"full\",\"essentials\"],default=\"\
full\")\n a=p.parse_args()\n try: capture(a.namespace,a.pod,a.output,a.profile)\n\
\ except Exception as error:\n print('ERROR: Forgejo archive capture\
\ or integrity validation failed ('+type(error).__name__+')')\n return\
\ 1\n print('Forgejo archive capture and integrity verified')\n return 0\n\
\nif __name__=='__main__': raise SystemExit(main())\n"
lib__railiance-backup-common.sh: "#!/usr/bin/env bash\n# lib/railiance-backup-common.sh\
\ \u2014 age encrypt + Nextcloud upload helpers (Option A)\nset -euo pipefail\n\
\n: \"${RAILIANCE_BACKUP_AGE_PUBLIC_KEY:=age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz}\"\
\n: \"${RAILIANCE_BACKUP_BAO_PATH:=platform/workloads/railiance/backup/offsite-lane}\"\
\n: \"${RAILIANCE_BACKUP_NC_PREFIX:=forgejo}\"\n\nrailiance_backup_load_openbao_lane()\
\ {\n if [[ -n \"${RAILIANCE_BACKUP_NC_TOKEN:-}\" && -n \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:-}\"\
\ ]]; then\n return 0\n fi\n command -v bao >/dev/null 2>&1 || return 0\n\
\ bao kv metadata get \"${RAILIANCE_BACKUP_BAO_PATH}\" >/dev/null 2>&1 || return\
\ 0\n : \"${RAILIANCE_BACKUP_NC_TOKEN:=$(bao kv get -field=NC_WEBDAV_TOKEN \"\
${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=$(bao\
\ kv get -field=NC_WEBDAV_URL \"${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\
\n}\n\nrailiance_backup_require_openbao_lane() {\n railiance_backup_load_openbao_lane\n\
\ if [[ -z \"${RAILIANCE_BACKUP_NC_TOKEN:-}\" ]]; then\n echo \"ERROR: governed\
\ backup credential unavailable (CCR-2026-0004)\" >&2\n return 1\n fi\n #\
\ Build a file-drop URL only after the required token is present.\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/dav/filesdrop/${RAILIANCE_BACKUP_NC_TOKEN}}\"\
\n}\n\nrailiance_backup_require_tools() {\n # Prefer vendored static binaries\
\ (activity-core hostPath / offline runners).\n local root vendor\n root=\"\
$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n vendor=\"${root}/tools/vendor/bin\"\
\n if [[ -d \"$vendor\" ]]; then\n export PATH=\"${vendor}:${PATH}\"\n fi\n\
\ if [[ -n \"${RAILIANCE_BACKUP_AGE_BIN:-}\" && -x \"${RAILIANCE_BACKUP_AGE_BIN}\"\
\ ]]; then\n export PATH=\"$(dirname \"${RAILIANCE_BACKUP_AGE_BIN}\"):${PATH}\"\
\n fi\n local t\n for t in age kubectl; do\n command -v \"$t\" >/dev/null\
\ 2>&1 || { echo \"ERROR: missing required tool: $t (run tools/cmd/install-cnpg-backup-vendor-tools)\"\
\ >&2; exit 1; }\n done\n # curl optional \u2014 upload falls back to python3\
\ urllib (activity-core worker has no curl)\n if ! command -v curl >/dev/null\
\ 2>&1 && ! command -v python3 >/dev/null 2>&1; then\n echo \"ERROR: need curl\
\ or python3 for Nextcloud upload\" >&2\n exit 1\n fi\n}\n\nrailiance_backup_nc_upload()\
\ {\n local file=\"$1\" remote_name=\"$2\"\n local dest=\"${RAILIANCE_BACKUP_NC_WEBDAV_URL%/}/${RAILIANCE_BACKUP_NC_PREFIX}/${remote_name}\"\
\n if [[ \"$dest\" != https://* || \"$dest\" == *$'\\n'* || \"$dest\" == *$'\\\
r'* ||\n \"${RAILIANCE_BACKUP_NC_TOKEN}\" == *$'\\n'* || \"${RAILIANCE_BACKUP_NC_TOKEN}\"\
\ == *$'\\r'* ||\n \"$file\" == *$'\\n'* || \"$file\" == *$'\\r'* ]]; then\n\
\ echo \"ERROR: invalid backup upload input\" >&2\n return 1\n fi\n if\
\ command -v curl >/dev/null 2>&1; then\n # Credentials and credential-bearing\
\ URLs travel over stdin, never argv.\n local config_url config_token config_file\
\ status\n config_url=\"${dest//\\\\/\\\\\\\\}\"; config_url=\"${config_url//\\\
\"/\\\\\\\"}\"\n config_token=\"${RAILIANCE_BACKUP_NC_TOKEN//\\\\/\\\\\\\\\
}\"; config_token=\"${config_token//\\\"/\\\\\\\"}\"\n config_file=\"${file//\\\
\\/\\\\\\\\}\"; config_file=\"${config_file//\\\"/\\\\\\\"}\"\n status=\"$(printf\
\ 'url = \"%s\"\\nuser = \"%s:\"\\nupload-file = \"%s\"\\n' \\\n \"$config_url\"\
\ \"$config_token\" \"$config_file\" | \\\n curl --disable --silent --fail\
\ --proto '=https' --max-time 600 \\\n --header 'X-Requested-With: XMLHttpRequest'\
\ \\\n --output /dev/null --write-out '%{http_code}' --config - 2>/dev/null)\"\
\ \\\n || { echo \"ERROR: Nextcloud upload failed\" >&2; return 1; }\n \
\ case \"$status\" in\n 200|201|204) ;;\n *) echo \"ERROR: Nextcloud\
\ upload failed\" >&2; return 1 ;;\n esac\n return 0\n fi\n # Python fallback\
\ (worker image)\n RAILIANCE_BACKUP_NC_TOKEN=\"${RAILIANCE_BACKUP_NC_TOKEN}\"\
\ \\\n RAILIANCE_BACKUP_UPLOAD_URL=\"${dest}\" \\\n RAILIANCE_BACKUP_UPLOAD_FILE=\"\
${file}\" \\\n python3 - <<'PY' || { echo \"ERROR: Nextcloud upload failed\"\
\ >&2; return 1; }\nimport os, urllib.request, urllib.error, base64, sys\nurl\
\ = os.environ[\"RAILIANCE_BACKUP_UPLOAD_URL\"]\npath = os.environ[\"RAILIANCE_BACKUP_UPLOAD_FILE\"\
]\ntoken = os.environ[\"RAILIANCE_BACKUP_NC_TOKEN\"]\nclass NoRedirect(urllib.request.HTTPRedirectHandler):\n\
\ def redirect_request(self, req, fp, code, msg, headers, newurl):\n \
\ raise urllib.error.HTTPError(req.full_url, code, 'redirect refused', headers,\
\ fp)\ntry:\n with open(path, 'rb') as source:\n chunks = iter(lambda:\
\ source.read(1024 * 1024), b'')\n req = urllib.request.Request(url, data=chunks,\
\ method=\"PUT\")\n req.add_header(\"Authorization\", \"Basic \" + base64.b64encode(f\"\
{token}:\".encode()).decode())\n req.add_header(\"X-Requested-With\", \"\
XMLHttpRequest\")\n req.add_header(\"Content-Type\", \"application/octet-stream\"\
)\n req.add_header(\"Content-Length\", str(os.fstat(source.fileno()).st_size))\n\
\ with urllib.request.build_opener(NoRedirect()).open(req, timeout=600)\
\ as resp:\n if resp.status not in (200, 201, 204):\n \
\ sys.exit(1)\nexcept Exception:\n # URLs may themselves contain credentials:\
\ never emit exception text.\n sys.exit(1)\nPY\n}\n\nrailiance_backup_encrypt()\
\ {\n local src=\"$1\" dest=\"$2\"\n age -r \"${RAILIANCE_BACKUP_AGE_PUBLIC_KEY}\"\
\ -o \"$dest\" \"$src\"\n}\n\nrailiance_backup_prune_local() {\n local dir=\"\
$1\" pattern=\"$2\" keep=\"${3:-7}\"\n find \"$dir\" -maxdepth 1 -name \"$pattern\"\
\ -type f 2>/dev/null \\\n | sort -r | tail -n +\"$((keep + 1))\" | xargs -r\
\ rm -f\n}\n\nrailiance_backup_record_success() {\n local stamp_dir=\"$1\" ts=\"\
$2\"\n mkdir -p \"$stamp_dir\"\n echo \"$ts\" > \"${stamp_dir}/.last-success\"\
\n echo \"$ts\" >> \"${stamp_dir}/success-log\"\n}\n"
lib__railiance-print.sh: "#!/usr/bin/env bash\n# lib/railiance-print.sh \u2014 shared\
\ pretty printers for platform operator tools\nset -euo pipefail\n\nprint_hdr()\
\ { printf \"\\n%s\\n\" \"$1\"; printf \"%0.s-\" $(seq 1 \"${#1}\"); echo; }\n\
ok() { printf \" \u2705 %-12s %s\\n\" \"$1\" \"$2\"; }\nwarn() { printf \"\
\ \u26A0\uFE0F %-12s %s\\n\" \"$1\" \"$2\"; }\nbad() { printf \" \u274C %-12s\
\ %s\\n\" \"$1\" \"$2\"; }"
entrypoint: '#!/bin/sh
exec /opt/railiance-backup-verified/tools/cmd/forgejo-backup "$@"
'

View file

@ -0,0 +1,29 @@
spec:
template:
spec:
volumes:
- name: backup-verified
configMap:
name: backup-verified-debb9e4c59c9
defaultMode: 365
items:
- key: tools__cmd__forgejo-backup
path: tools/cmd/forgejo-backup
- key: scripts__capture_forgejo_archive.py
path: scripts/capture_forgejo_archive.py
- key: lib__railiance-backup-common.sh
path: lib/railiance-backup-common.sh
- key: lib__railiance-print.sh
path: lib/railiance-print.sh
- key: entrypoint
path: entrypoint
containers:
- name: worker
volumeMounts:
- name: backup-verified
mountPath: /opt/railiance-backup-verified
readOnly: true
- name: backup-verified
mountPath: /opt/railiance-platform/tools/cmd/forgejo-backup
subPath: entrypoint
readOnly: true

View file

@ -267,7 +267,7 @@ id: ACTIVITY-WP-0042-T08
status: wait
depends_on: [RPF-WP-0049-T05]
priority: high
blocking_reason: "Platform must supply an authenticated replacement-lane/CAS and encrypted history upload/readback receipt; then a reviewed scoped worker bundle/hostPath and env refresh is required."
blocking_reason: "Provider prerequisite verified at lane KV4; scoped consumer bundle/hostPath and env refresh remain unperformed. Exact primary projection also blocked by existing historical identifier compatibility in repo-manager."
state_hub_task_id: "020d19be-0576-5052-ad35-14b1d5a48fe1"
```
@ -316,3 +316,21 @@ CAS from lane version 3 to 4; accept only the actual verified publication
receipt, not that forecast. `runtime_consumers_refreshed: false` correctly
leaves this T08 pending. Shared header source is `50da37c`; no broader backup
readiness follows from initial root-history provider proof.
2026-10-03T19:56:59Z supersedes the missing replacement-lane prerequisite:
producer repair succeeded, lane CAS3->4 changed only token/URL, existing
escrow/unrelated fields preserved, encrypted upload201/readback200 and
independent decryption verified. Non-secret receipt retained in
`docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json`.
`runtime_consumers_refreshed: false`; T08 remains waiting solely on reviewed
consumer delivery and its acceptance. Root-history provider proof is complete.
Concrete review packet `k8s/backup-release/README.md` and its source-owned
immutable Forgejo bundle/worker patch are staged outside ordinary GitOps
rendering. No runtime apply/restart occurred. CNPG requires separate primary
checkout delivery. `rmgr sync --path . --push` currently fails preflight on
existing historic `custodian-WP-*` identifiers, before projection; source Git
and local Hub sync succeed. Do not renumber grandfathered IDs or claim exact
primary projection. This existing repo-manager compatibility issue must be
resolved by its owner before that projection-based rollout path.