trading-system-docs/tolaria/c5-sync-service/delete_worker.py
Rain Ocampo 9c2d6e365a AUTH.4D: DELETE-scoped executor + delete_request_id binding in SoT core
- delete_worker.py: DELETE-scoped worker loop (ApprovalStateStore, fail-closed)
- delete_tolaria_client.py: DELETE-only Tolaria client (fixed /delete endpoint)
- delete_entrypoint.py: DELETE runtime entrypoint (health/status + worker)
- delete_Dockerfile: DELETE executor image (no SAVE code, no credentials)
- delete_executor_core.py: minimal SoT patch — VALID_NONCE replay guard +
  VALID_DELETE_REQUEST binding (approval binds delete_request_id, fail-closed
  APPROVAL_MISMATCH). Closes P10/T29 security gap in productive SoT.

AUTH.4D P14. No deployment, no token injection, no key provisioning.
2026-08-28 02:28:48 +00:00

225 lines
9 KiB
Python

"""
AUTH.4D — worker.py (DELETE-scoped)
===================================
DELETE-Worker-Loop für den c5-delete-executor.
Ablauf (pro Job):
poll -> atomic claim -> validate -> load approval -> verify AUTH.3D
signature (Ed25519 PUBLIC KEY ONLY) -> check expiry/nonce/single-use ->
check object/path/commit/provenance/mission binding -> execute Tolaria
DELETE on FIXED endpoint -> verify/audit -> complete
Eigenschaften:
* DELETE-only: claimt NUR C5_DELETE_OBJECT-Jobs.
* Der DELETE-Job selbst ist KEINE Approval. Job allein autorisiert NIE.
* Kein SAVE. Keine write()-Capability. Kein generic dispatcher.
* Kein shell/python/arbitrary URL/method/headers.
* POLL_INTERVAL, LEASE, MAX_ATTEMPTS, structured backoff, graceful SIGTERM.
* OUTCOME_UNKNOWN bei unklarem Delete-Ergebnis. KEIN blinder Retry.
* Fail-closed: ohne DELETE-Credential wird kein HTTP-DELETE ausgeführt.
"""
from __future__ import annotations
import logging
import os
import signal
import sys
import time
from typing import Any, Dict, Optional
from approval_payload import parse_payload
from approval_signature import public_key_from_pem
from approval_state import ApprovalStateStore
from approval_verifier import ApprovalVerifier, ApprovalVerificationError
from delete_executor_core import DeleteExecutorCore
from job_store import JobStore
from delete_tolaria_client import TolariaClient
SCOPE = "DELETE"
DB_PATH = os.environ.get("C5_DELETE_DB", "/data/c5a_delete.db")
VERSION = "0.1.0-auth4d-wired"
# Konfiguration (ENV mit konservativen Defaults)
POLL_INTERVAL = float(os.environ.get("C5_POLL_INTERVAL", "5.0"))
LEASE_SECONDS = int(os.environ.get("C5_LEASE_SECONDS", "60"))
MAX_ATTEMPTS = int(os.environ.get("C5_MAX_ATTEMPTS", "3"))
BACKOFF_BASE = float(os.environ.get("C5_BACKOFF_BASE", "2.0"))
BACKOFF_MAX = float(os.environ.get("C5_BACKOFF_MAX", "60.0"))
# Tolaria-Client (fixed base URL, DELETE-only)
TOLARIA_BASE = os.environ.get("C5_TOLARIA_BASE", "http://tolaria:5173/api/vault")
TOLARIA_DELETE_TOKEN = os.environ.get("TOLARIA_DELETE_TOKEN")
# AUTH.3D Public Key (verify-only). NIE ein Private Key.
# Produktiv: PEM des Christian Public Key (Human-Gate-Phase). Fehlt -> fail-closed.
PUBLIC_VERIFY_KEY_PEM = os.environ.get("C5_DELETE_PUBLIC_KEY_PEM")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger("c5-delete-worker")
# Graceful Shutdown
_shutdown = False
def _handle_sigterm(signum, frame):
global _shutdown
log.info("SIGTERM empfangen — Worker fährt sauber herunter.")
_shutdown = True
def _handle_sigint(signum, frame):
global _shutdown
log.info("SIGINT empfangen — Worker fährt sauber herunter.")
_shutdown = True
signal.signal(signal.SIGTERM, _handle_sigterm)
signal.signal(signal.SIGINT, _handle_sigint)
def _backoff_delay(attempt: int) -> float:
"""Structured exponential backoff (kein blinder Retry, nur transient)."""
delay = min(BACKOFF_MAX, BACKOFF_BASE * (2 ** (attempt - 1)))
return delay
def _build_verifier() -> Optional[ApprovalVerifier]:
"""Baut den AUTH.3D-Verifier aus dem Public Key (verify-only).
Fehlender/ungültiger Public Key -> None (fail-closed: keine Approval
kann verifiziert werden, kein DELETE wird ausgeführt).
"""
if not PUBLIC_VERIFY_KEY_PEM or not PUBLIC_VERIFY_KEY_PEM.strip():
return None
try:
pub = public_key_from_pem(PUBLIC_VERIFY_KEY_PEM.encode("utf-8"))
except Exception as e:
log.error("Ungültiger Public Key PEM: %s", e)
return None
from approval_signature import key_id
return ApprovalVerifier(public_keys={key_id(pub): pub})
def _build_worker() -> tuple[JobStore, DeleteExecutorCore]:
"""Baut Store + Core mit injizierten Callables (Verdrahtung)."""
store = JobStore(DB_PATH, SCOPE)
client = TolariaClient(base_url=TOLARIA_BASE, delete_token=TOLARIA_DELETE_TOKEN)
verifier = _build_verifier()
# AUTH.3D Approval-State-Store (Single-Use / Nonce / Reservation).
# In Produktion: C5-DB (delete_approvals). Hier: In-Memory (isoliert).
approval_store = ApprovalStateStore()
def approval_loader(approval_id: str) -> Optional[Dict[str, Any]]:
# Approval wird aus dem Approval-State-Store geladen (approval_id).
# Kein externer Approval-Service, keine Approval im Job-Payload.
return approval_store.get(approval_id)
def approval_verify(approval: Dict[str, Any]) -> Dict[str, Any]:
# AUTH.3D-Verifikation (Ed25519 PUBLIC KEY ONLY).
# Fail-closed: kein Verifier / kein Public Key -> DELETE_DENIED.
if verifier is None:
return {"valid": False, "code": "DELETE_DENIED", "reason": "no public key"}
try:
result = verifier.verify_approval(
approval["raw_payload"],
approval["signature"],
expected_mission_id=approval["mission_id"],
expected_delete_request_id=approval["delete_request_id"],
expected_object_id=approval["object_id"],
expected_vault_path=approval["vault_path"],
expected_commit=approval["expected_commit"],
expected_provenance_hash=approval["expected_provenance_hash"],
expected_nonce=approval["nonce"],
consumed=approval.get("consumed", False),
state_allows_delete=approval.get("state_allows_delete", True),
used_nonces=approval.get("used_nonces", set()),
)
return {"valid": True, "reason": "ok", "key_id": result.get("key_id")}
except ApprovalVerificationError as e:
return {"valid": False, "code": e.reason_code, "reason": e.message}
except Exception as e:
return {"valid": False, "code": "DELETE_DENIED", "reason": str(e)}
def tolaria_delete(vault_path: str) -> Dict[str, Any]:
try:
client.delete(vault_path)
return {"status": "ok"}
except Exception as e:
# Unklar vs. bestätigt unterscheiden
code = getattr(e, "code", "TOLARIA_UNAVAILABLE")
if code == "CREDENTIAL_MISSING":
return {"status": "error", "code": "CREDENTIAL_MISSING", "uncertain": False}
if code in ("AUTH_FAILURE", "INVALID_SCHEMA"):
return {"status": "error", "code": code, "uncertain": False}
# Netzwerk/Timeout/5xx -> unklar (kein blinder Retry)
return {"status": "error", "code": "TOLARIA_UNAVAILABLE", "uncertain": True}
core = DeleteExecutorCore(store, approval_loader, approval_verify, tolaria_delete)
return store, core
def _process_ready_job(store: JobStore, core: DeleteExecutorCore, job: Dict[str, Any]) -> None:
"""Verarbeitet einen READY-Job (mit MAX_ATTEMPTS + backoff)."""
job_id = job["job_id"]
attempt = 0
while attempt < MAX_ATTEMPTS:
attempt += 1
log.info("Verarbeite Job %s (Versuch %d/%d)", job_id, attempt, MAX_ATTEMPTS)
try:
result = core.process_job(job_id, f"worker-{os.getpid()}")
state = result.get("state")
log.info("Job %s -> %s (result_code=%s)",
job_id, state, result.get("result_code"))
# Terminal-States: fertig. OUTCOME_UNKNOWN: kein blinder Retry.
if state in ("SUCCEEDED", "FAILED", "REJECTED", "OUTCOME_UNKNOWN"):
return
# READY/CLAIMED/EXECUTING (transient) -> backoff + erneut versuchen
if attempt < MAX_ATTEMPTS:
time.sleep(_backoff_delay(attempt))
except Exception as e:
log.error("Job %s Fehler: %s", job_id, e)
if attempt < MAX_ATTEMPTS:
time.sleep(_backoff_delay(attempt))
# MAX_ATTEMPTS erschöpft -> OUTCOME_UNKNOWN (kein blinder Retry)
try:
store.mark_outcome_unknown(job_id, f"worker-{os.getpid()}")
except Exception:
pass
def main() -> int:
log.info("c5-delete-worker v%s startet (scope=%s, poll=%.1fs, lease=%ds, max_attempts=%d)",
VERSION, SCOPE, POLL_INTERVAL, LEASE_SECONDS, MAX_ATTEMPTS)
if not TOLARIA_DELETE_TOKEN:
log.warning("TOLARIA_DELETE_TOKEN fehlt — fail-closed: kein HTTP-DELETE wird ausgeführt.")
if not PUBLIC_VERIFY_KEY_PEM:
log.warning("C5_DELETE_PUBLIC_KEY_PEM fehlt — fail-closed: keine Approval kann verifiziert werden.")
store, core = _build_worker()
try:
while not _shutdown:
try:
# READY-Jobs claimen und verarbeiten
ready_jobs = store.list_jobs(state="READY")
for job in ready_jobs:
if _shutdown:
break
_process_ready_job(store, core, job)
except Exception as e:
log.error("Worker-Loop-Fehler: %s", e)
# Poll-Intervall (graceful: bei SIGTERM sofort raus)
for _ in range(int(POLL_INTERVAL * 10)):
if _shutdown:
break
time.sleep(0.1)
finally:
store.close()
log.info("Worker beendet.")
return 0
if __name__ == "__main__":
sys.exit(main())