- 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.
103 lines
3.7 KiB
Python
103 lines
3.7 KiB
Python
"""
|
|
AUTH.3E — job_state_machine.py
|
|
===============================
|
|
Deterministische Job-State-Machine für den Executor Command Channel.
|
|
|
|
States:
|
|
CREATED, READY, CLAIMED, EXECUTING, SUCCEEDED, FAILED, REJECTED, OUTCOME_UNKNOWN
|
|
|
|
Eigenschaften:
|
|
* Geschlossene Transition-Allowlist
|
|
* Ungültige Transition -> FAIL CLOSED (InvalidTransitionError)
|
|
* DELETE: KEIN RETRYABLE — ein gestarteter DELETE wird bei Lease-Expiry in
|
|
OUTCOME_UNKNOWN überführt, NICHT zurück zu READY (kein blinder Retry)
|
|
* SAVE: RETRYABLE nur bei transienten Fehlern (TolariaUnavailable)
|
|
|
|
Isoliert implementiert (KEIN produktiver Container).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, FrozenSet, Optional, Tuple
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# States
|
|
# ---------------------------------------------------------------------------
|
|
ST_CREATED = "CREATED"
|
|
ST_READY = "READY"
|
|
ST_CLAIMED = "CLAIMED"
|
|
ST_EXECUTING = "EXECUTING"
|
|
ST_SUCCEEDED = "SUCCEEDED"
|
|
ST_FAILED = "FAILED"
|
|
ST_REJECTED = "REJECTED"
|
|
ST_OUTCOME_UNKNOWN = "OUTCOME_UNKNOWN"
|
|
|
|
ALL_STATES = frozenset({
|
|
ST_CREATED, ST_READY, ST_CLAIMED, ST_EXECUTING,
|
|
ST_SUCCEEDED, ST_FAILED, ST_REJECTED, ST_OUTCOME_UNKNOWN,
|
|
})
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Erlaubte Transitionen (geschlossene Allowlist)
|
|
# ---------------------------------------------------------------------------
|
|
# (from_state, to_state)
|
|
ALLOWED_TRANSITIONS: FrozenSet[Tuple[str, str]] = frozenset({
|
|
# CREATED
|
|
(ST_CREATED, ST_READY), # Schema validiert, Job akzeptiert
|
|
(ST_CREATED, ST_REJECTED), # Schema/Allowlist/Injection-Fehler
|
|
# READY
|
|
(ST_READY, ST_CLAIMED), # atomarer Claim
|
|
(ST_READY, ST_REJECTED), # Gate-Fehler vor Claim
|
|
# CLAIMED
|
|
(ST_CLAIMED, ST_EXECUTING), # Claim bestätigt
|
|
(ST_CLAIMED, ST_REJECTED), # Gate-Fehler nach Claim
|
|
# EXECUTING
|
|
(ST_EXECUTING, ST_SUCCEEDED), # bestätigter Erfolg
|
|
(ST_EXECUTING, ST_FAILED), # bestätigter Fehler, kein Mutationseffekt
|
|
(ST_EXECUTING, ST_OUTCOME_UNKNOWN), # unklares HTTP-Ergebnis
|
|
(ST_EXECUTING, ST_REJECTED), # Gate-Fehler während Execution
|
|
# OUTCOME_UNKNOWN (terminal für DELETE; SAVE kann via Human/Admin reaktiviert werden)
|
|
(ST_OUTCOME_UNKNOWN, ST_REJECTED), # expliziter Human-/Admin-Eingriff
|
|
})
|
|
|
|
# Terminal-States (keine weiteren Transitionen ohne expliziten Eingriff)
|
|
TERMINAL_STATES = frozenset({ST_SUCCEEDED, ST_FAILED, ST_REJECTED})
|
|
|
|
|
|
class InvalidTransitionError(ValueError):
|
|
"""Ungültige Transition -> FAIL CLOSED."""
|
|
|
|
|
|
def is_valid_transition(from_state: str, to_state: str) -> bool:
|
|
return (from_state, to_state) in ALLOWED_TRANSITIONS
|
|
|
|
|
|
def transition(from_state: str, to_state: str) -> str:
|
|
"""
|
|
Führt eine State-Transition aus. Wirft InvalidTransitionError bei ungültiger
|
|
Transition (FAIL CLOSED).
|
|
"""
|
|
if from_state not in ALL_STATES:
|
|
raise InvalidTransitionError(f"unknown from_state: {from_state!r}")
|
|
if to_state not in ALL_STATES:
|
|
raise InvalidTransitionError(f"unknown to_state: {to_state!r}")
|
|
if not is_valid_transition(from_state, to_state):
|
|
raise InvalidTransitionError(
|
|
f"invalid transition: {from_state} -> {to_state} (FAIL CLOSED)"
|
|
)
|
|
return to_state
|
|
|
|
|
|
def is_terminal(state: str) -> bool:
|
|
return state in TERMINAL_STATES
|
|
|
|
|
|
def is_retryable(state: str, job_type: str) -> bool:
|
|
"""
|
|
SAVE: RETRYABLE nur aus OUTCOME_UNKNOWN (transient) — via Human/Admin.
|
|
DELETE: NIE retryable — ein gestarteter DELETE wird nicht blind wiederholt.
|
|
"""
|
|
if job_type == "C5_DELETE_OBJECT":
|
|
return False
|
|
# SAVE
|
|
return state == ST_OUTCOME_UNKNOWN
|