trading-system-docs/tolaria/c5-sync-service/save_executor_core.py
Red Queen 10761f52b2 AUTH.3E: Executor Command Channel + Runtime Boundary Contract
- job_schema: geschlossene Job-Type-Allowlist (C5_SAVE_OBJECT/C5_DELETE_OBJECT), Pfad-/Längen-Validierung
- job_state_machine: deterministische States (CREATED/READY/CLAIMED/EXECUTING/SUCCEEDED/FAILED)
- job_claim: atomare Claim-/Lease-/Recovery-Logik (kein TOCTOU)
- job_store: getrennte SQLite-Inbox-DBs (c5a_save.db/c5a_delete.db), delegiert an job_claim
- save_executor_core: SAVE-only, Content-Rekonstruktion, Provenance-Validierung
- delete_executor_core: DELETE-only, AUTH.3D-Composition, TOCTOU-Defense (Re-Read nach Claim)
- test_job_channel: T1-T40 + adversarial (72 Tests)
- test_job_channel_adversarial: adversarial + Substitution + DB-Manipulation
- sensitivity_proof_auth3e: Mutationen A-L (12/12 Invarianten PRESENT)
- AUTH3B/3C/3D/3E_DESIGN: autoritative Security-Dokumentation (e25 Reconciliation)

COMMAND != AUTHORIZATION. Kein generischer Dispatcher. RQ credential-free.
Keine produktive Mutation. Keine echten Credentials.
2026-08-27 10:03:32 +00:00

144 lines
5.3 KiB
Python

"""
AUTH.3E — save_executor_core.py
================================
SAVE-Executor-Logik (SAVE-only).
Eigenschaften:
* SAVE-only: claimt NUR C5_SAVE_OBJECT-Jobs
* Content-Rekonstruktion: lädt Content selbst aus autoritativer Source (Forgejo),
statt RQ blind zu vertrauen (DATA FROM RQ != AUTHORITY)
* RQ darf NICHT bestimmen: Tolaria Base URL, HTTP Method, Authorization Header,
Credential, beliebigen Zielendpoint
* Fail-closed: kein HTTP ohne SAVE-Credential
* OUTCOME_UNKNOWN bei unklarem HTTP-Ergebnis (kein blinder Retry)
Isoliert implementiert (KEIN produktiver Container). Nutzt job_store + job_schema.
"""
from __future__ import annotations
import hashlib
from typing import Any, Callable, Dict, Optional
from job_schema import JOB_TYPE_SAVE
from job_store import JobStore
# ---------------------------------------------------------------------------
# Result-Codes
# ---------------------------------------------------------------------------
RC_OK = "OK"
RC_CREDENTIAL_MISSING = "CREDENTIAL_MISSING"
RC_SOURCE_UNAVAILABLE = "SOURCE_UNAVAILABLE"
RC_PROVENANCE_MISMATCH = "PROVENANCE_MISMATCH"
RC_STATE_MISMATCH = "STATE_MISMATCH"
RC_PATH_INVALID = "PATH_INVALID"
RC_TOLARIA_UNAVAILABLE = "TOLARIA_UNAVAILABLE"
RC_OUTCOME_UNKNOWN = "OUTCOME_UNKNOWN"
RC_REJECTED = "REJECTED"
class SaveExecutorError(Exception):
pass
class SaveExecutorCore:
"""
SAVE-Executor. worker_scope="SAVE" (nur SAVE-Jobs claimbar).
source_loader: Callable[[str, str], Optional[str]] — lädt Content aus
autoritativer Source (source_commit, object_id) -> content oder None.
tolaria_save: Callable[[str, str], Dict[str, Any]] — führt den Tolaria-SAVE
aus (vault_path, content) -> {"status": "ok"|"error", "uncertain": bool}.
Muss fail-closed sein (kein HTTP ohne Credential).
"""
def __init__(
self,
store: JobStore,
source_loader: Callable[[str, str], Optional[str]],
tolaria_save: Callable[[str, str], Dict[str, Any]],
):
if store.worker_scope != "SAVE":
raise SaveExecutorError("SaveExecutorCore requires worker_scope='SAVE'")
self.store = store
self.source_loader = source_loader
self.tolaria_save = tolaria_save
# -- Hauptverarbeitung --------------------------------------------------
def process_job(self, job_id: str, worker_id: str) -> Dict[str, Any]:
"""
Verarbeitet einen SAVE-Job durch die State Machine.
Ablauf:
1. Job laden (muss existieren)
2. Job-Type-Scope prüfen (nur SAVE)
3. READY -> CLAIMED (atomarer Claim)
4. CLAIMED -> EXECUTING
5. Content aus autoritativer Source rekonstruieren
6. Provenance/State validieren
7. Tolaria-SAVE ausführen
8. Ergebnis: SUCCEEDED / FAILED / OUTCOME_UNKNOWN / REJECTED
"""
job = self.store.get_job(job_id)
if job is None:
raise SaveExecutorError(f"job not found: {job_id}")
# Scope: nur SAVE-Jobs
if job["job_type"] != JOB_TYPE_SAVE:
self.store.mark_rejected(job_id, RC_REJECTED)
return self.store.get_job(job_id)
# Atomarer Claim
try:
self.store.claim_job(job_id, worker_id)
except Exception:
# Nicht claimbar (bereits geclaimt) -> kein Doppel-Write
return self.store.get_job(job_id)
self.store.begin_execution(job_id, worker_id)
# Content-Rekonstruktion aus autoritativer Source
content = self._reconstruct_content(job)
if content is None:
self.store.mark_failed(job_id, RC_SOURCE_UNAVAILABLE, worker_id)
return self.store.get_job(job_id)
# Provenance validieren (RECOMPUTE)
if not self._validate_provenance(job, content):
self.store.mark_failed(job_id, RC_PROVENANCE_MISMATCH, worker_id)
return self.store.get_job(job_id)
# Tolaria-SAVE ausführen (fail-closed)
result = self.tolaria_save(job["vault_path"], content)
status = result.get("status")
if status == "ok":
self.store.mark_succeeded(job_id, worker_id)
elif status == "error" and result.get("uncertain"):
self.store.mark_outcome_unknown(job_id, worker_id)
else:
self.store.mark_failed(job_id, result.get("code", RC_TOLARIA_UNAVAILABLE), worker_id)
return self.store.get_job(job_id)
# -- Content-Rekonstruktion ---------------------------------------------
def _reconstruct_content(self, job: Dict[str, Any]) -> Optional[str]:
"""
Lädt Content selbst aus autoritativer Source (source_commit, object_id).
RQ liefert KEINEN Content im Job — nur Referenzen.
"""
try:
return self.source_loader(job["source_commit"], job["object_id"])
except Exception:
return None
# -- Provenance-Validierung ---------------------------------------------
def _validate_provenance(self, job: Dict[str, Any], content: str) -> bool:
"""
RECOMPUTE: berechnet den Provenance-Hash aus dem rekonstruierten Content
und vergleicht mit dem Job-Feld. RQ darf den Hash nicht blind bestimmen.
"""
computed = hashlib.sha256(content.encode("utf-8")).hexdigest()
return computed == job["provenance_hash"]