trading-system-docs/red-queen-architecture/control-plane/cp2a2_2b/rq-historical

132 lines
5.2 KiB
Bash

#!/bin/bash
# ============================================================================
# rq-historical — eng begrenzter root-Wrapper fuer Red Queen (rq-host)
# RED-QUEEN-VPS-HOST-ACCESS + PHASE-13.5-READONLY | Rain Ocampo (Hermes) | 2026-08-29
#
# EINZIGER ZWECK: kontrollierter READ-ONLY-Zugriff + freigegebener Backfill-
# Start ausschliesslich auf den Historical-Service. Least-Privilege.
#
# SICHERHEIT:
# - Container-/Skript-Namen sind FEST im Script (kein Benutzer-Argument
# fuer Container/Skript/Pfad).
# - Alle Subkommandos akzeptieren NUR strikt validierte, vollstaendig
# konsumierte Argumente; unbekannte/ueberzaehlige Argumente -> ERR.
# - code/hashes/code-list nutzen eine FESTE Allowlist: User uebergibt nur
# einen Key, der intern auf einen festen Containerpfad gemappt wird.
# KEIN frei durchgereichter Pfad, KEIN Shell-String aus User-Input.
# - Argumente werden als argv-Array an `docker exec` uebergeben
# (KEINE Shell-Interpolation von Benutzereingaben).
# - KEIN sudo ALL / NOPASSWD ALL / keine Root-Shell / kein docker run.
#
# AUFRUF (ueber /etc/sudoers.d/rq-historical, NUR als root):
# sudo rq-historical ps
# sudo rq-historical inspect
# sudo rq-historical logs [--tail N]
# sudo rq-historical run [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--limit-days N]
# sudo rq-historical code <runner|backfill|pipeline|repository|provenance|trust|sanity|trust_gate>
# sudo rq-historical hashes
# sudo rq-historical code-list
# ============================================================================
set -euo pipefail
CONTAINER=historical-service
# ------------------------------------------------------------------ Allowlist
# FESTE Mapping-Tabelle (Key -> Containerpfad). Read-only, hart im Script.
# Keys NICHT benutzerdefiniert erweiterbar.
# NOTE: Bei assign-assoc-array die Assignments OHNE Leerzeichen um '=' schreiben,
# sonst Syntaxfehler. (Korrektur unten: einzeilig mit declare -A init.)
declare -A CODE_FILES=(
[runner]="/app/app/run_backfill_year.py"
[backfill]="/app/app/backfill.py"
[pipeline]="/app/app/pipeline_v2.py"
[repository]="/app/app/persistence/repository.py"
[provenance]="/app/app/trust/provenance.py"
[trust]="/app/app/trust/trust.py"
[sanity]="/app/app/trust/sanity.py"
[trust_gate]="/app/app/trust/trust_gate.py"
)
# Relevanter Pfad fuer den 'run'-Subbefehl (Backfill-Entrypoint)
SCRIPT=/app/app/run_backfill_year.py
# ---------------------------------------------------------------- Validierung
die() { echo "ERR: $*" >&2; exit 2; }
req_ok() {
docker inspect -f '{{.State.Running}}' "$CONTAINER" >/dev/null 2>&1 \
|| die "Container '$CONTAINER' nicht vorhanden oder nicht am Laufen"
}
# -------------------------------------------------------------------- dispatch
SUBCMD="${1:-}"
shift || true
case "$SUBCMD" in
ps)
[ $# -eq 0 ] || die "unerlaubtes Argument 'ps $*'"
exec docker ps
;;
inspect)
req_ok
[ $# -eq 0 ] || die "unerlaubtes Argument 'inspect $*'"
exec docker inspect "$CONTAINER"
;;
logs)
req_ok
tail_n=200
while [ $# -gt 0 ]; do
case "$1" in
--tail)
[ $# -ge 2 ] || die "--tail braucht Wert"
[[ "$2" =~ ^[0-9]+$ ]] || die "ungueltiges --tail"
tail_n="$2"; shift 2 ;;
*) die "unerlaubtes Argument 'logs $1'" ;;
esac
done
exec docker logs --tail "$tail_n" "$CONTAINER"
;;
run)
req_ok
declare -a RUN=()
while [ $# -gt 0 ]; do
case "$1" in
--start) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --start"; RUN+=("--start" "$2"); shift 2 ;;
--end) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --end"; RUN+=("--end" "$2"); shift 2 ;;
--limit-days) [[ "${2:-}" =~ ^[0-9]+$ ]] || die "ungueltiges --limit-days"; RUN+=("--limit-days" "$2"); shift 2 ;;
*) die "unerlaubtes Argument 'run $1'" ;;
esac
done
echo "[rq-historical] Start: docker exec $CONTAINER python $SCRIPT ${RUN[*]}" >&2
exec docker exec "$CONTAINER" python "$SCRIPT" "${RUN[@]}"
;;
code)
# Fester Allowlist-Key -> fester Containerpfad. KEIN frei gereichter Pfad.
req_ok
[ $# -eq 1 ] || die "code braucht genau EINEN Key (siehe code-list)"
key="$1"
path="${CODE_FILES[$key]:-}"
[ -n "$path" ] || die "unbekannter Key 'code $key' (siehe code-list)"
exec docker exec "$CONTAINER" cat "$path"
;;
hashes)
# SHA256 der festen Allowlist-Dateien aus dem laufenden Container.
req_ok
[ $# -eq 0 ] || die "unerlaubtes Argument 'hashes $*'"
# Feste, vollstaendig konsumierte Dateiliste -> argv-Array (keine Interpolation).
declare -a PATHS=()
for k in "${!CODE_FILES[@]}"; do PATHS+=("${CODE_FILES[$k]}"); done
exec docker exec "$CONTAINER" sha256sum "${PATHS[@]}"
;;
code-list)
# Nur die verfuegbaren fixen Keys + Containerpfade anzeigen. Kein FS-Zugriff.
req_ok
[ $# -eq 0 ] || die "unerlaubtes Argument 'code-list $*'"
echo "Verfuegbare READ-ONLY-Code-Keys (historical-service):"
for k in "${!CODE_FILES[@]}"; do
printf ' %-11s -> %s\n' "$k" "${CODE_FILES[$k]}"
done
;;
*)
die "Aufruf: rq-historical {ps|inspect|logs [--tail N]|run [--start --end --limit-days]|code <KEY>|hashes|code-list}"
;;
esac