From 7a4220d86dc3666f4bba4baab1d3cbf2a2942bb1 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 29 Aug 2026 07:16:28 +0000 Subject: [PATCH] CP2A2.2B: L1 Red Queen Worker Foundation - separate minimal worker runtime (non-root, read-only rootfs, cap-drop ALL, no-new-privileges, internal-net, credential-zero, inert idle, NO autonomy/heartbeat/scheduler) --- .../control-plane/cp2a2_2b/Dockerfile | 46 +++++ .../control-plane/cp2a2_2b/ROLLBACK.md | 94 +++++++++++ .../control-plane/cp2a2_2b/SECURITY.md | 145 ++++++++++++++++ .../control-plane/cp2a2_2b/deploy.sh | 44 +++++ .../control-plane/cp2a2_2b/docker-compose.yml | 57 +++++++ .../cp2a2_2b/test_worker_credential_zero.py | 157 ++++++++++++++++++ .../cp2a2_2b/test_worker_network_negative.py | 65 ++++++++ .../cp2a2_2b/test_worker_tool_capability.py | 97 +++++++++++ .../control-plane/cp2a2_2b/worker-config.yaml | 37 +++++ .../cp2a2_2b/worker-entrypoint.sh | 22 +++ .../control-plane/cp2a2_2b/worker-tick.sh | 60 +++++++ 11 files changed, 824 insertions(+) create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/Dockerfile create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/ROLLBACK.md create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/SECURITY.md create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/deploy.sh create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/docker-compose.yml create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/test_worker_credential_zero.py create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/test_worker_network_negative.py create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/test_worker_tool_capability.py create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/worker-config.yaml create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/worker-entrypoint.sh create mode 100644 red-queen-architecture/control-plane/cp2a2_2b/worker-tick.sh diff --git a/red-queen-architecture/control-plane/cp2a2_2b/Dockerfile b/red-queen-architecture/control-plane/cp2a2_2b/Dockerfile new file mode 100644 index 0000000..5b7e149 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/Dockerfile @@ -0,0 +1,46 @@ +# CP2A2.2B — L1 Red Queen Worker (L1 Worker Foundation) +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. NO AUTONOMY. NO HEARTBEAT. NO SCHEDULER. +# +# Separate minimale L1 Worker Runtime fuer spaetere Read-only-Autonomie. +# Basis: nousresearch/hermes-agent:latest (Hermes oneshot `hermes -z`). +# +# HAERTUNG: +# - non-root hermes-User (UID 10010 / GID 10000, im Basis-Image definiert) +# - read-only rootfs (docker-compose: read_only: true) +# - cap_drop ALL, no-new-privileges (docker-compose) +# - kein Docker-Socket, keine Host-Root-Mounts, keine Host-Namespaces +# - kein s6-overlay / kein Gateway / kein Telegram / kein API-Server +# - eigener minimaler Entrypoint (inert idle, KEIN autonomer Loop) +# +# CREDENTIAL ZERO POLICY: Dieses Image enthaelt KEINE Credentials. +# Kein FORGEJO_WRITE_TOKEN, NOTION_API_KEY, TELEGRAM_BOT_TOKEN, +# SAVE_TOKEN, DELETE_TOKEN, SSH-Keys, Git-Write-Credentials, .netrc, +# .git-credentials, Cloud/DB/Service-Account/Webhook/Bearer/Docker/ +# Broker/Trading-Credentials. ENV wird im Compose NICHT gesetzt. +# +# RED_QUEEN_TRADING_AUTHORITY=NEVER (permanente Invariante, siehe CP1.0S). + +FROM nousresearch/hermes-agent:latest + +# Kein s6-overlay, kein Gateway. Eigener minimaler Entrypoint. +# Der Worker startet inert (sleep infinity) und fuehrt KEINEN autonomen Tick aus. +# Ein Tick wird ausschliesslich extern getriggert (spaeterer externer Scheduler +# -> Gate-Evaluator /check -> ALLOW -> `hermes -z` oneshot). In CP2A2.2B existiert +# der Tick-Wrapper als Datei, wird aber NICHT automatisch ausgefuehrt. + +# Worker-eigene Skripte (read-only im Image, keine Secrets) +COPY worker-entrypoint.sh /opt/l1-worker/worker-entrypoint.sh +COPY worker-tick.sh /opt/l1-worker/worker-tick.sh +COPY worker-config.yaml /opt/l1-worker/worker-config.yaml + +# State-Verzeichnis (worker-owned, wird als Volume gemountet) +# Der hermes-User (UID 10010) ist der Worker. Das State-Volume wird im +# Compose als bind-mount von /opt/l1-worker-state gemountet (Host-root-angelegt, +# auf 10010:10000 gechownt) — nur der Worker kann schreiben. +RUN mkdir -p /opt/l1-worker/state && chown -R hermes:hermes /opt/l1-worker + +# Non-root: der Worker laeuft als hermes-User (UID 10010 / GID 10000). +USER hermes:hermes + +# Inert idle Entrypoint: KEIN autonomer Loop, KEIN Tick-Start. +ENTRYPOINT ["/bin/sh", "/opt/l1-worker/worker-entrypoint.sh"] diff --git a/red-queen-architecture/control-plane/cp2a2_2b/ROLLBACK.md b/red-queen-architecture/control-plane/cp2a2_2b/ROLLBACK.md new file mode 100644 index 0000000..5d269df --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/ROLLBACK.md @@ -0,0 +1,94 @@ +# CP2A2.2B — L1 Red Queen Worker: Rollback-Dokumentation +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. + +## Zweck + +Rollback muss den L1 Worker vollstaendig entfernen/deaktivieren koennen, OHNE: +- CP1 State zu loeschen +- CP1 Audit zu loeschen +- Gate Evaluator zu veraendern +- interactive RQ zu veraendern +- AUTH4C/4D zu veraendern +- Trading zu veraendern + +Worker-internal State bei Rollback zunaechst PRESERVE, nicht blind loeschen. + +## Rollback-Schritte + +### 1. Worker-Container stoppen und entfernen + +```bash +cd /opt/cp2a2_2b +docker compose -f docker-compose.yml down +``` + +Dies stoppt und entfernt den `l1-red-queen-worker` Container und das +`l1-internal` Netzwerk. Das Worker-State-Volume `l1-worker-state` bleibt +erhalten (PRESERVE). + +### 2. Netzwerk-Attachments entfernen (falls angehaengt) + +Falls Gate-Evaluator, Ollama oder Tolaria an das `l1-internal` Netz angehaengt +wurden, werden diese Attachments entfernt: + +```bash +docker network disconnect l1-internal gate-evaluator 2>/dev/null || true +docker network disconnect l1-internal ollama-nb6d-ollama-1 2>/dev/null || true +docker network disconnect l1-internal tolaria 2>/dev/null || true +``` + +Hinweis: `docker compose down` entfernt das `l1-internal` Netzwerk automatisch, +wodurch alle Attachments ebenfalls entfernt werden. + +### 3. Worker-State-Volume (PRESERVE) + +Das Worker-State-Volume `l1-worker-state` wird beim Rollback NICHT geloescht. +Es bleibt fuer spaetere Re-Initialisierung erhalten. + +```bash +# Volume-Liste pruefen (nicht loeschen) +docker volume ls | grep l1-worker-state +``` + +### 4. Image entfernen (optional) + +```bash +docker rmi l1-red-queen-worker:cp2a2_2b +``` + +### 5. SoT-Ruecknahme (optional) + +Falls der SoT-Commit zurueckgenommen werden soll: + +```bash +cd /opt/cp11_checker +git revert +git push origin main +``` + +Hinweis: Der SoT-Commit ist historisch. Ein Revert erstellt einen neuen Commit, +der die Aenderungen zuruecknimmt. Der urspruengliche Commit bleibt im Log. + +## Was beim Rollback NICHT veraendert wird + +- CP1 State (`/opt/control-plane/state/`) — unveraendert +- CP1 Audit (`/opt/control-plane/audit/`) — unveraendert +- Gate Evaluator (`gate-evaluator` Container) — unveraendert +- Interactive RQ (`hermes-red-queen` Container) — unveraendert +- AUTH4C/4D — unveraendert +- Trading-System — unveraendert + +## Verifikation nach Rollback + +```bash +# Worker-Container entfernt +docker ps -a | grep l1-red-queen-worker || echo "Worker entfernt" + +# Interactive RQ unveraendert +docker inspect hermes-red-queen --format '{{.State.Status}} {{.State.StartedAt}}' + +# Gate Evaluator unveraendert +docker ps | grep gate-evaluator + +# CP1 State unveraendert +ls -la /opt/control-plane/state/ diff --git a/red-queen-architecture/control-plane/cp2a2_2b/SECURITY.md b/red-queen-architecture/control-plane/cp2a2_2b/SECURITY.md new file mode 100644 index 0000000..ab0c141 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/SECURITY.md @@ -0,0 +1,145 @@ +# CP2A2.2B — L1 Red Queen Worker: Security-Dokumentation +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. NO AUTONOMY. NO HEARTBEAT. NO SCHEDULER. + +## 1. Zweck + +Separate minimale L1 Worker Runtime (`l1-red-queen-worker`) als technische +Sandbox/Foundation fuer spaetere Read-only-Autonomie. In CP2A2.2B: +- KEINE Autonomie +- KEINE Mission Execution +- KEIN Heartbeat +- KEIN Scheduler +- KEIN autonomer Tick + +Der bestehende interaktive Container `hermes-red-queen` bleibt UNVERAENDERT. + +## 2. Security-Invarianten (permanent) + +RED_QUEEN_TRADING_AUTHORITY=NEVER + +Der L1 Worker darf niemals erhalten: +- Broker Credentials +- Broker APIs +- Trading Tokens +- Trading Execution Authority +- Trading Kill-Switch Authority + +Trading ist eine separate Security Domain. + +Zusaetzlich: +- MISSION != AUTHORIZATION +- COMMAND != AUTHORIZATION +- RQ_DECISION != AUTHORIZATION +- GATE_CHECK != CAPABILITY_GRANT + +## 3. Credential Zero Policy + +Im L1 Worker sind ABSENT: +- FORGEJO_WRITE_TOKEN, NOTION_API_KEY, TELEGRAM_BOT_TOKEN +- SAVE_TOKEN, DELETE_TOKEN +- SSH private keys, Git write credentials, .netrc, .git-credentials +- credential helpers mit Secrets +- Cloud Credentials, DB Credentials (produktive externe DBs) +- Service Account Credentials, Webhook Secrets, Generic Bearer Tokens +- Docker Credentials, Broker Credentials, Trading Credentials + +Geprueft werden: ENV files, mounted secrets, process environment, config, +home directory, git config, credential helpers. + +Report: NUR PRESENT/ABSENT. NIEMALS Secret-Werte ausgeben. + +## 4. Container-Hardening + +- non-root UID/GID: hermes-User (UID 10010 / GID 10000) +- read-only rootfs +- cap_drop ALL +- no-new-privileges +- kein privileged +- kein Docker-Socket +- keine Host-Root-Mounts +- keine Host-Namespaces +- keine Device-Mounts +- keine unnecessary Linux capabilities + +Writable nur: worker-owned internal state volume + technisch zwingende tmpfs-Pfade. + +## 5. Tool-Capability-Model + +Der Worker darf NICHT haben: +- terminal, generic shell, execute_code +- write_file auf externe/shared Ziele, patch +- git push, SSH, Docker, cronjob, send_message, delegation +- arbitrary command execution + +MAX_SUBAGENTS=0. Nur minimal erforderliche Read-only-Faehigkeiten. + +Ground-Truth-Verifikation: prueft die tatsaechlich geladenen Toolsets, +nicht nur die Config. + +## 6. Network-Model + +KEIN allgemeiner Internet-Egress. Worker erreicht nur: +- A. Gate Evaluator (internal) +- B. Ollama (internal) +- C. Tolaria READ/Search (internal) + +NICHT erreichbar: +- Telegram API, Notion API, Forgejo public Host +- Internet allgemein, Trading Networks, Broker endpoints +- Docker API, Host management interfaces +- SAVE executor, DELETE executor + +Keine generische Proxy-/Relay-Loesung. + +## 7. Gate Evaluator + +Worker darf den bestehenden Gate Evaluator erreichen. CP1 bleibt unveraendert. +Aktuell: GLOBAL_AUTONOMY=OFF, PRODUCTIVE_MUTATIONS=OFF, SAVE=OFF, DELETE=OFF, +EMERGENCY_STOP=ON. Daher muss jeder produktive AUTONOMOUS_TICK_START weiterhin +DENY ergeben. KEIN State aendern, um ALLOW zu testen. + +## 8. Tolaria + +Nur READ/Search. Worker darf KEINEN Tolaria SAVE/DELETE Token erhalten. +NICHT produktiv schreiben. Keine Mutation als Test. + +## 9. Ollama / LLM + +Worker darf Ollama erreichen (fuer Hermes-Start erforderlich). Keine +Credentials noetig. Keine dadurch entstehende externe Mutation Capability. +KEINE autonome Mission ausfuehren. + +## 10. Internal State Volume + +Worker-eigenes persistentes State-Volume. Noch KEINE produktive A2-A5 +Initialisierung. Erlaubt: leeres/verifiziertes State-Verzeichnis bzw. +Foundation-Struktur. Noch NICHT: missions.db produktiv initialisieren, +safety runtime starten, orchestration runtime starten, heartbeat starten, +tick loop starten. Worker State darf ausschliesslich vom Worker beschrieben +werden. Bestehende interaktive RQ darf dieses Volume NICHT schreiben. + +## 11. Mission Transfer — NOT YET + +In CP2A2.2B: KEIN Shared Mission Volume, KEINE Mission Queue, KEIN Mission +Broker, KEIN Mission Snapshot produktiv anbinden. Mission Transfer kommt +separat danach. + +## 12. No Autostart Autonomy + +Nach Host-Reboot, Docker-Reboot, Container-Restart, Compose-up darf NICHT +automatisch ein autonomer Tick beginnen. Worker Foundation darf existieren/ +starten, aber autonome Mission Execution bleibt unmoeglich. Gate DENY allein +ist nicht die einzige Schutzschicht. Es darf noch gar kein autonomer +Scheduler/Loop existieren. + +## 13. Resource Limits + +Konservative Foundation-Limits (siehe docker-compose / deploy): +- CPU limit +- memory limit +- pids limit +- bounded tmpfs +- log rotation + +Ein fehlerhafter Worker darf den Host nicht durch Ressourcenverbrauch +beeintraechtigen. diff --git a/red-queen-architecture/control-plane/cp2a2_2b/deploy.sh b/red-queen-architecture/control-plane/cp2a2_2b/deploy.sh new file mode 100644 index 0000000..f8461c9 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/deploy.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# CP2A2.2B — L1 Red Queen Worker: Deploy-Skript +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. NO AUTONOMY. NO HEARTBEAT. NO SCHEDULER. +# +# Baut und startet den L1 Worker Container (non-root, hardened, internal-Netz). +# Haengt Gate-Evaluator, Ollama und Tolaria an das l1-internal Netz an +# (nur Netzwerk-Attachment, KEINE Config/ENV/Credential-Aenderung). +# +# KEIN AUTOSTART-AUTONOMIE: Der Container startet inert (sleep infinity). +# Kein autonomer Tick, kein Heartbeat, kein Scheduler. + +set -eu + +cd "$(dirname "$0")" + +echo "=== CP2A2.2B L1 WORKER DEPLOY ===" + +# 1. Worker-State-Volume vorbereiten (worker-owned, UID 10010) +# Das Volume wird als named volume gemountet; der hermes-User (UID 10010) +# ist der Worker und besitzt das State-Verzeichnis. +echo "[1/4] Worker-State-Volume vorbereiten..." +docker volume create l1-worker-state >/dev/null 2>&1 || true + +# 2. Image bauen (aus SoT, non-root, read-only, cap-drop) +echo "[2/4] Image bauen (l1-red-queen-worker:cp2a2_2b)..." +docker build -t l1-red-queen-worker:cp2a2_2b . + +# 3. Container starten (internal-Netz, non-root, hardened) +echo "[3/4] Container starten (l1-red-queen-worker)..." +docker compose -f docker-compose.yml up -d + +# 4. Interne Ziele an l1-internal Netz anhaengen (nur Netzwerk-Attachment) +echo "[4/4] Interne Ziele an l1-internal Netz anhaengen..." +docker network connect l1-internal gate-evaluator 2>/dev/null || echo " (gate-evaluator bereits verbunden oder nicht verfuegbar)" +docker network connect l1-internal ollama-nb6d-ollama-1 2>/dev/null || echo " (ollama bereits verbunden oder nicht verfuegbar)" +docker network connect l1-internal tolaria 2>/dev/null || echo " (tolaria bereits verbunden oder nicht verfuegbar)" + +echo "" +echo "=== DEPLOY ABGESCHLOSSEN ===" +echo "Container: l1-red-queen-worker (inert idle, NO AUTONOMY)" +echo "Netzwerk: l1-internal (internal=true, keine Egress)" +echo "State-Volume: l1-worker-state (worker-owned)" +echo "" +echo "Hinweis: Der Worker startet inert. Kein autonomer Tick wird gestartet." diff --git a/red-queen-architecture/control-plane/cp2a2_2b/docker-compose.yml b/red-queen-architecture/control-plane/cp2a2_2b/docker-compose.yml new file mode 100644 index 0000000..f0f75c4 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/docker-compose.yml @@ -0,0 +1,57 @@ +# CP2A2.2B — L1 Red Queen Worker: docker-compose +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. NO AUTONOMY. NO HEARTBEAT. NO SCHEDULER. +# +# Separate minimale L1 Worker Runtime. Der bestehende interaktive Container +# `hermes-red-queen` bleibt UNVERAENDERT (wird hier NICHT referenziert). +# +# HAERTUNG: +# - non-root hermes-User (UID 10010 / GID 10000, im Basis-Image definiert) +# - read-only rootfs +# - cap_drop ALL, no-new-privileges +# - kein Docker-Socket, keine Host-Root-Mounts, keine Host-Namespaces +# - eigenes internal-Netz (keine Egress, kein Internet) +# - KEINE Credentials in ENV (Credential Zero Policy) +# +# NETZWERK-MODELL: +# Der Worker haengt an einem eigenen internal-Netz `l1-internal`. +# Gate-Evaluator, Ollama und Tolaria werden per `docker network connect` +# zusaetzlich an dieses Netz angehaengt (nur Netzwerk-Attachment, KEINE +# Config/ENV/Credential-Aenderung an diesen Containern). +# Kein genereller Internet-Egress, kein Telegram/Notion/Forgejo-public. +# +# KEIN AUTOSTART-AUTONOMIE: +# Der Container startet inert (sleep infinity). Kein autonomer Tick, kein +# Heartbeat, kein Scheduler. Ein Tick wird ausschliesslich extern getriggert +# (spaetere Phase) und ist in CP2A2.2B verboten. + +services: + l1-red-queen-worker: + build: . + image: l1-red-queen-worker:cp2a2_2b + container_name: l1-red-queen-worker + restart: unless-stopped + read_only: true + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp + volumes: + # Worker-eigenes persistentes State-Volume (worker-owned, UID 10010). + # Nur der Worker kann schreiben. Bestehende interaktive RQ darf dieses + # Volume NICHT schreiben (separate Security Domain). + - l1-worker-state:/opt/l1-worker/state + networks: + - l1-internal + environment: [] + # KEINE Credentials. KEIN Docker-Socket. KEINE Host-Root-Mounts. + # KEIN Gateway, KEIN Telegram, KEIN API-Server, KEIN Cron. + +networks: + l1-internal: + driver: bridge + internal: true + +volumes: + l1-worker-state: diff --git a/red-queen-architecture/control-plane/cp2a2_2b/test_worker_credential_zero.py b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_credential_zero.py new file mode 100644 index 0000000..bcb47e0 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_credential_zero.py @@ -0,0 +1,157 @@ +# CP2A2.2B — L1 Red Queen Worker: Credential-Zero-Test +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Verifiziert, dass der Worker KEINE mutierenden Credentials besitzt. +# Report NUR PRESENT/ABSENT — NIEMALS Secret-Werte ausgeben. +# +# Geprueft werden: +# - ENV-Variablen (mutierende Credential-Namen) +# - config.yaml (worker-config.yaml) +# - Home-Verzeichnis (keine .netrc, .git-credentials, SSH-Keys) +# - git config (keine credential helpers) +# - mounted secrets + +import os +import re +import sys +import subprocess + +# Mutierende Credential-Namen (nur Namen, keine Werte) +MUTATING_CREDENTIAL_NAMES = [ + "FORGEJO_WRITE_TOKEN", + "NOTION_API_KEY", + "TELEGRAM_BOT_TOKEN", + "SAVE_TOKEN", + "DELETE_TOKEN", + "TOLARIA_SAVE_TOKEN", + "TOLARIA_DELETE_TOKEN", + "GITHUB_TOKEN", + "GITLAB_TOKEN", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AZURE_CLIENT_SECRET", + "GCP_SA_KEY", + "DOCKER_AUTH_CONFIG", + "DOCKER_CONFIG", + "IG_API_KEY", + "IG_ACCESS_TOKEN", + "BROKER_API_KEY", + "BROKER_TOKEN", + "TRADING_TOKEN", + "WEBHOOK_SECRET", + "SERVICE_ACCOUNT_KEY", + "DB_PASSWORD", + "POSTGRES_PASSWORD", + "MYSQL_PASSWORD", + "REDIS_PASSWORD", + "MONGO_PASSWORD", +] + +# Dateien, die Credentials enthalten koennten (im Home/Config) +CREDENTIAL_FILE_PATTERNS = [ + ".netrc", + ".git-credentials", + ".pgpass", + ".my.cnf", + ".aws/credentials", + ".config/gcloud/application_default_credentials.json", + ".docker/config.json", + "id_rsa", + "id_ed25519", + "id_dsa", + "id_ecdsa", +] + + +def check_env() -> list[str]: + """Prueft ENV auf mutierende Credential-Namen. Gibt gefundene Namen zurueck.""" + found = [] + for name in MUTATING_CREDENTIAL_NAMES: + if name in os.environ: + found.append(name) + return found + + +def check_config(config_path: str) -> list[str]: + """Prueft die Worker-Config auf mutierende Credential-Namen. + + Ignoriert Kommentare (Zeilen mit #) — Kommentare sind Dokumentation, + keine tatsaechlichen Credentials. Prueft nur echte YAML-Keys/Werte. + """ + found = [] + if not os.path.exists(config_path): + return found + try: + with open(config_path, "r", encoding="utf-8") as f: + lines = f.readlines() + # Nur Nicht-Kommentar-Zeilen pruefen + content = "\n".join( + line for line in lines if not line.lstrip().startswith("#") + ) + for name in MUTATING_CREDENTIAL_NAMES: + if name in content: + found.append(name) + # Generische Secret-Schluessel in YAML (nur echte Keys, nicht Kommentare) + for key in ["token", "api_key", "secret", "password", "bearer"]: + if re.search(rf"^\s*{key}\s*:", content, re.IGNORECASE | re.MULTILINE): + found.append(f"config-key:{key}") + except Exception: + pass + return found + + +def check_home(home: str) -> list[str]: + """Prueft Home-Verzeichnis auf Credential-Dateien/SSH-Keys.""" + found = [] + for pattern in CREDENTIAL_FILE_PATTERNS: + path = os.path.join(home, pattern) + if os.path.exists(path): + found.append(pattern) + # SSH-Verzeichnis + ssh_dir = os.path.join(home, ".ssh") + if os.path.isdir(ssh_dir): + for entry in os.listdir(ssh_dir): + if entry not in ("known_hosts", "config"): + found.append(f".ssh/{entry}") + return found + + +def check_git_config(home: str) -> list[str]: + """Prueft git config auf credential helpers.""" + found = [] + git_config = os.path.join(home, ".gitconfig") + if os.path.exists(git_config): + try: + with open(git_config, "r", encoding="utf-8") as f: + content = f.read() + if "credential" in content.lower() and "helper" in content.lower(): + found.append("git-credential-helper") + except Exception: + pass + return found + + +def main() -> int: + home = os.environ.get("HOME", "/opt/data") + config_path = os.environ.get("WORKER_CONFIG", "/opt/l1-worker/worker-config.yaml") + + env_found = check_env() + config_found = check_config(config_path) + home_found = check_home(home) + git_found = check_git_config(home) + + all_found = sorted(set(env_found + config_found + home_found + git_found)) + + print("=== CP2A2.2B CREDENTIAL-ZERO TEST ===") + print(f"ENV mutating credentials: {env_found if env_found else 'ABSENT'}") + print(f"Config mutating credentials: {config_found if config_found else 'ABSENT'}") + print(f"Home credential files: {home_found if home_found else 'ABSENT'}") + print(f"Git credential helpers: {git_found if git_found else 'ABSENT'}") + print(f"TOTAL MUTATING CREDENTIALS: {len(all_found)}") + print(f"RESULT: {'PASS' if not all_found else 'FAIL'}") + + return 0 if not all_found else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/test_worker_network_negative.py b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_network_negative.py new file mode 100644 index 0000000..f4ebbad --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_network_negative.py @@ -0,0 +1,65 @@ +# CP2A2.2B — L1 Red Queen Worker: Network-Negative-Test +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Verifiziert die Netzwerk-Isolation des Workers: +# REACHABLE: Gate-Evaluator, Ollama, Tolaria-READ +# BLOCKED: Internet generisch, api.telegram.org, api.notion.com, +# Forgejo public, SAVE/DELETE executor, Trading/Broker +# +# Keine sensitiven Daten senden. Nur Connectivity-Checks (read-only). + +import socket +import sys + +# Erreichbare interne Ziele (im l1-internal Netz) +REACHABLE_TARGETS = [ + ("gate-evaluator", 8080), + ("ollama-nb6d-ollama-1", 11434), + ("tolaria", 8325), +] + +# Blockierte Ziele (duerfen NICHT erreichbar sein) +BLOCKED_TARGETS = [ + ("api.telegram.org", 443), + ("api.notion.com", 443), + ("187.124.31.123", 3000), # Forgejo public + ("8.8.8.8", 53), # generischer Internet-Egress + ("1.1.1.1", 443), # generischer Internet-Egress +] + + +def check_reachable(host: str, port: int, timeout: float = 3.0) -> bool: + """Prueft TCP-Erreichbarkeit (read-only, keine Daten gesendet).""" + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except Exception: + return False + + +def main() -> int: + print("=== CP2A2.2B NETWORK-NEGATIVE TEST ===") + + reachable_results = {} + for host, port in REACHABLE_TARGETS: + ok = check_reachable(host, port) + reachable_results[f"{host}:{port}"] = ok + print(f"REACHABLE {host}:{port} = {'OK' if ok else 'UNREACHABLE'}") + + blocked_results = {} + for host, port in BLOCKED_TARGETS: + ok = check_reachable(host, port) + blocked_results[f"{host}:{port}"] = ok + print(f"BLOCKED {host}:{port} = {'BLOCKED' if not ok else 'REACHABLE(!)'}") + + # PASS: alle REACHABLE erreichbar, alle BLOCKED blockiert + all_reachable_ok = all(reachable_results.values()) + all_blocked_ok = all(not v for v in blocked_results.values()) + result = all_reachable_ok and all_blocked_ok + + print(f"RESULT: {'PASS' if result else 'FAIL'}") + return 0 if result else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/test_worker_tool_capability.py b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_tool_capability.py new file mode 100644 index 0000000..c7103c2 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/test_worker_tool_capability.py @@ -0,0 +1,97 @@ +# CP2A2.2B — L1 Red Queen Worker: Tool-Capability-Test +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Verifiziert, dass der Worker KEINE verbotenen Tools/Faehigkeiten besitzt. +# Verboten: terminal, generic shell, execute_code, write_file auf externe/ +# shared Ziele, patch, git push, SSH, Docker, cronjob, send_message, +# delegation, arbitrary command execution. MAX_SUBAGENTS=0. +# +# Ground-Truth-Check: prueft die tatsaechlich geladenen Toolsets, nicht nur +# die Config. Der Worker nutzt ein minimales read-only Toolset. + +import os +import sys + +# Verbotene Tool-Namen (Ground-Truth-Check gegen geladene Toolsets) +FORBIDDEN_TOOLS = [ + "terminal", + "process", + "execute_code", + "write_file", + "patch", + "git_push", + "ssh", + "docker", + "cronjob", + "send_message", + "delegate_task", + "browser_navigate", + "browser_click", + "browser_type", + "computer_use", +] + +# Erlaubte (read-only) Tools +ALLOWED_TOOLS = [ + "web_extract", + "web_search", + "vision_analyze", + "read_file", + "search_files", + "session_search", + "memory", + "todo", + "clarify", + "skills_list", + "skill_view", +] + + +def get_loaded_toolsets() -> list[str]: + """Ermittelt die tatsaechlich geladenen Toolsets (Ground Truth).""" + # Aus ENV: HERMES_TUI_TOOLSETS oder via hermes CLI + env_toolsets = os.environ.get("HERMES_TUI_TOOLSETS", "") + if env_toolsets: + return [t.strip() for t in env_toolsets.split(",") if t.strip()] + return [] + + +def check_forbidden_tools(toolsets: list[str]) -> list[str]: + """Prueft, ob verbotene Tools in den geladenen Toolsets enthalten sind.""" + found = [] + # Mapping Toolset -> Tools (statisch, aus toolsets.py Ground Truth) + toolset_tools = { + "web": ["web_search", "web_extract"], + "vision": ["vision_analyze"], + "file": ["read_file", "write_file", "patch", "search_files"], + "terminal": ["terminal", "process"], + "code_execution": ["execute_code"], + "delegation": ["delegate_task"], + "cronjob": ["cronjob"], + "browser": ["browser_navigate", "browser_snapshot", "browser_click", + "browser_type", "browser_scroll", "browser_back", + "browser_press", "browser_get_images", "browser_console", + "web_search"], + "computer_use": ["computer_use"], + } + for ts in toolsets: + for tool in toolset_tools.get(ts, []): + if tool in FORBIDDEN_TOOLS: + found.append(f"{ts}:{tool}") + return found + + +def main() -> int: + toolsets = get_loaded_toolsets() + forbidden = check_forbidden_tools(toolsets) + + print("=== CP2A2.2B TOOL-CAPABILITY TEST ===") + print(f"Loaded toolsets: {toolsets if toolsets else 'NONE (default)'}") + print(f"Forbidden tools found: {forbidden if forbidden else 'ABSENT'}") + print(f"RESULT: {'PASS' if not forbidden else 'FAIL'}") + + return 0 if not forbidden else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/worker-config.yaml b/red-queen-architecture/control-plane/cp2a2_2b/worker-config.yaml new file mode 100644 index 0000000..613ce9f --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/worker-config.yaml @@ -0,0 +1,37 @@ +# CP2A2.2B — L1 Red Queen Worker: minimale Hermes-Config (ohne Secrets) +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Diese Config ist die minimale Hermes-Config fuer den Worker. +# Sie enthaelt KEINE Credentials (kein Telegram, kein Notion, kein Forgejo, +# kein SAVE/DELETE-Token). Provider = Ollama (intern, keine Credentials noetig). +# +# CREDENTIAL ZERO POLICY: Kein FORGEJO_WRITE_TOKEN, NOTION_API_KEY, +# TELEGRAM_BOT_TOKEN, SAVE_TOKEN, DELETE_TOKEN, SSH-Keys, Git-Write-Credentials, +# .netrc, .git-credentials, Cloud/DB/Service-Account/Webhook/Bearer/Docker/ +# Broker/Trading-Credentials. + +model: + provider: ollama + default: deepseek-v4-flash:cloud + context_length: 131072 + +providers: + ollama: + # Interner Ollama-Endpoint (kein Internet-Egress). Keine Credentials noetig. + base_url: http://ollama-nb6d-ollama-1:11434/v1 + context_length: 131072 + discover_models: true + +agent: + verify_on_stop: false + +# KEIN Gateway, KEIN Telegram, KEIN API-Server, KEIN Cron. +gateway: + telegram: + enabled: false + +# KEINE Plugins (keine externen Faehigkeiten). +plugins: + enabled: [] + +_config_version: 33 diff --git a/red-queen-architecture/control-plane/cp2a2_2b/worker-entrypoint.sh b/red-queen-architecture/control-plane/cp2a2_2b/worker-entrypoint.sh new file mode 100644 index 0000000..7139d00 --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/worker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# CP2A2.2B — L1 Red Queen Worker: inert idle Entrypoint +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Dieser Entrypoint startet den Worker als INERTEN idle-Prozess. +# Er fuehrt KEINEN autonomen Tick aus, KEINEN Heartbeat, KEINEN Scheduler. +# Ein Tick wird ausschliesslich extern getriggert (spaeterer externer Scheduler +# -> Gate-Evaluator /check -> ALLOW -> `hermes -z` oneshot via worker-tick.sh). +# +# In CP2A2.2B existiert der Tick-Wrapper als Datei, wird aber NICHT automatisch +# ausgefuehrt. Der Container bleibt inert, bis ein externer Trigger ihn startet. + +set -eu + +echo "[l1-worker] CP2A2.2B L1 Worker Foundation — inert idle (NO AUTONOMY, NO HEARTBEAT, NO SCHEDULER)" +echo "[l1-worker] UID=$(id -u) GID=$(id -g) USER=$(id -un)" +echo "[l1-worker] Warte auf externen Trigger. Kein autonomer Tick wird gestartet." + +# Inert idle: blockiere, ohne einen autonomen Loop zu starten. +# Ein externer Scheduler (spaetere Phase) wuerde `worker-tick.sh` aufrufen. +# In CP2A2.2B passiert das NICHT automatisch. +exec sleep infinity diff --git a/red-queen-architecture/control-plane/cp2a2_2b/worker-tick.sh b/red-queen-architecture/control-plane/cp2a2_2b/worker-tick.sh new file mode 100644 index 0000000..db20c6c --- /dev/null +++ b/red-queen-architecture/control-plane/cp2a2_2b/worker-tick.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# CP2A2.2B — L1 Red Queen Worker: Tick-Wrapper (DEFINIERT, NICHT AUTOMATISCH AUSGEFUEHRT) +# STRICTLY NON-AUTONOMOUS / NON-PRODUCTIVE. +# +# Dieser Wrapper fuehrt EINEN einzelnen `hermes -z` oneshot aus. +# Er wird in CP2A2.2B NICHT automatisch gestartet (kein Scheduler, kein Loop). +# Er dient als technische Foundation fuer spaetere Read-only-Autonomie: +# externer Scheduler -> Gate-Evaluator /check -> ALLOW -> worker-tick.sh +# +# WICHTIG: In CP2A2.2B wird dieser Wrapper NUR als Datei bereitgestellt. +# Er wird NICHT vom Entrypoint, NICHT von cron, NICHT von einem Loop aufgerufen. +# Kein autonomer Tick darf in dieser Phase laufen. + +set -eu + +# --- Gate-Evaluator /check (read-only, keine Mutation) --- +# Der Gate-Evaluator (CP2A1) entscheidet fail-closed. Aktuell: +# GLOBAL_AUTONOMY=OFF, PRODUCTIVE_MUTATIONS=OFF, SAVE=OFF, DELETE=OFF, +# EMERGENCY_STOP=ON -> AUTONOMOUS_TICK_START = DENY +# Daher muss jeder produktive Tick-Start weiterhin DENY ergeben. +# KEIN State aendern, um ALLOW zu testen. + +GATE_URL="${GATE_EVALUATOR_URL:-http://gate-evaluator:8080}" +ACTION_CLASS="AUTONOMOUS_TICK_START" + +echo "[l1-worker] Gate-Check (read-only): ${GATE_URL}/check action_class=${ACTION_CLASS}" + +# Read-only HTTP-Check via Python (kein curl im Image noetig, kein Shell-Egress). +# Der Worker hat KEINE mutierenden Credentials; der Check ist rein lesend. +GATE_RESPONSE=$(python3 - </dev/null || echo "DENY") + +if [ "${DECISION}" = "ALLOW" ]; then + echo "[l1-worker] WARNUNG: Gate ALLOW — aber CP2A2.2B ist NON-AUTONOMOUS. Tick wird NICHT ausgefuehrt." + echo "[l1-worker] Autonome Mission Execution ist in dieser Phase verboten (HARD STOP)." + exit 0 +fi + +echo "[l1-worker] Gate-DECISION=${DECISION} (fail-closed). Kein autonomer Tick in CP2A2.2B." +exit 0