- DeleteExecutor (rq_c5_delete.py): Pre-Gates, Read-Back, idempotenter replay - TolariaClient.delete() (rq_c5c.py): kontrollierter DELETE, keine Probes - Approval-Store + Reason Codes RC_DELETE_APPROVAL_MISSING/MISMATCH (rq_c5a.py) - CLI: c5-delete-approve/execute/replay/status (rq_c5_cli.py) - C5E: recover()/replay() DELETE-Integration - C5D: verify_integrity prueft secret_blocked_objects (FAIL CLOSED) - Security: Path-Traversal-Block in _normalize_vault_path - Drift nach DELETE -> FAIL CLOSED zurueck zu HUMAN_REVIEW_REQUIRED - Tests: test_c5_delete (19), test_c5_delete_integration (22), test_c5_delete_fresh_checker (17) — alle gruen - ADR: C5_DELETE_EXECUTION_ARCHITECTURE_DECISION.md (ACCEPTED)
598 lines
23 KiB
Python
598 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Red Queen — C5 CLI (C5A + C5B + C5C): kontrollierte Inspektion & Dry-Run.
|
|
|
|
KEIN produktiver Poll-Daemon. KEINE daemon/start-forever-Funktion.
|
|
Nur explizite, begrenzte Befehle:
|
|
* poll-once — ein kontrollierter Poll-Zyklus (read-only)
|
|
* show-pending — ausstehende Commits anzeigen
|
|
* show-commit — Details eines Commits anzeigen
|
|
* dry-run-diff — Diff eines Commits anzeigen (read-only)
|
|
* health — Health-Contract anzeigen
|
|
* no-write-check — No-Downstream-Write-Guarantee pruefen
|
|
* propagate-plan — Propagation-Plan fuer einen Commit (read-only, KEIN Write)
|
|
* live-dry-run — Read-only Live-Dry-Run gegen realen Forgejo + Tolaria Stand
|
|
* c5c-guarantees — No-Search- und No-Master-Write-Guarantee pruefen (statisch)
|
|
* apply-commit — Commit vervollstaendigen (Search -> APPLIED)
|
|
* c5d-plan — C5D-Plan (UPDATING_SEARCH / RETRY_PENDING) anzeigen
|
|
* c5d-guarantees — C5D-Guarantees pruefen (statisch)
|
|
* c5e-recover — C5E: Recovery-Entscheidung (read-only)
|
|
* c5e-replay — C5E: Replay/Recovery (fail-closed, kein Write ohne --allow-writes)
|
|
* c5e-reconcile — C5E: Read-only Reconciliation (Diagnose, KEIN Repair)
|
|
* c5e-observability— C5E: Observability-Felder (metadata-minimal)
|
|
* c5e-health — C5E: Ehrlicher Health-Contract (HEALTHY/DEGRADED/BLOCKED)
|
|
* c5e-evidence — C5E: Fehler-Evidence fuer einen Commit
|
|
* c5e-guarantees — C5E: No-Production-Activation-Garantie pruefen (statisch)
|
|
|
|
ALLE C5E-Befehle sind fail-closed: standardmaessig KEIN produktiver Write.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from rq_c5a import C5AStore, ST_APPLIED, ST_DELETE_APPROVED, OP_DELETE_REQUEST
|
|
from rq_c5b import (
|
|
C5BPoller,
|
|
GitReader,
|
|
assert_no_downstream_write,
|
|
DEFAULT_POLL_INTERVAL_SECONDS,
|
|
ENV_POLL_INTERVAL,
|
|
)
|
|
from rq_c5c import (
|
|
C5CPropagator,
|
|
C5CDryRun,
|
|
TolariaClient,
|
|
assert_no_search_calls,
|
|
assert_no_master_write,
|
|
DEFAULT_TOLARIA_BASE,
|
|
ENV_TOLARIA_BASE,
|
|
)
|
|
from rq_c5d import (
|
|
C5DEngine,
|
|
C5DDryRun,
|
|
SearchClient,
|
|
assert_no_tolaria_write,
|
|
assert_no_production_activation,
|
|
DEFAULT_SEARCH_BASE,
|
|
ENV_SEARCH_BASE,
|
|
ENV_SEARCH_TOKEN,
|
|
)
|
|
from rq_c5e import (
|
|
C5EStore,
|
|
C5EEngine,
|
|
C5EReconciler,
|
|
observability,
|
|
health_contract,
|
|
failure_evidence,
|
|
)
|
|
from rq_c5_delete import (
|
|
DeleteExecutor,
|
|
DeleteApprovalError,
|
|
DeleteExecutionError,
|
|
)
|
|
|
|
|
|
def _store(db_path: str) -> C5AStore:
|
|
return C5AStore(db_path)
|
|
|
|
|
|
def _c5e_store(db_path: str) -> C5EStore:
|
|
return C5EStore(db_path)
|
|
|
|
|
|
def _poller(store: C5AStore, repo_path: str, interval: Optional[int]) -> C5BPoller:
|
|
return C5BPoller(store, repo_path, poll_interval_seconds=interval)
|
|
|
|
|
|
def cmd_poll_once(args: argparse.Namespace) -> int:
|
|
store = _store(args.db)
|
|
poller = _poller(store, args.repo, args.interval)
|
|
result = poller.poll_once()
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_show_pending(args: argparse.Namespace) -> int:
|
|
store = _store(args.db)
|
|
pending = store.list_commits()
|
|
print(json.dumps(pending, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_show_commit(args: argparse.Namespace) -> int:
|
|
store = _store(args.db)
|
|
commit = store.get_commit(args.sha)
|
|
if commit is None:
|
|
print(f"Commit nicht gefunden: {args.sha}")
|
|
store.close()
|
|
return 1
|
|
print(json.dumps(commit, ensure_ascii=False, indent=2))
|
|
objs = store.list_object_changes(args.sha)
|
|
if objs:
|
|
print("--- Object Changes ---")
|
|
print(json.dumps(objs, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_dry_run_diff(args: argparse.Namespace) -> int:
|
|
reader = GitReader(args.repo)
|
|
changes = reader.diff_name_status(args.sha)
|
|
print(json.dumps(changes, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
|
|
def cmd_health(args: argparse.Namespace) -> int:
|
|
store = _store(args.db)
|
|
print(json.dumps(store.health(), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_no_write_check(args: argparse.Namespace) -> int:
|
|
result = assert_no_downstream_write()
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0 if result["no_downstream_write"] else 1
|
|
|
|
|
|
def _c5c_client(args: argparse.Namespace) -> TolariaClient:
|
|
return TolariaClient(base_url=args.tolaria_base)
|
|
|
|
|
|
def cmd_propagate_plan(args: argparse.Namespace) -> int:
|
|
"""Erzeugt den Propagation-Plan fuer einen Commit (read-only, KEIN Write)."""
|
|
store = _store(args.db)
|
|
reader = GitReader(args.repo)
|
|
dry = C5CDryRun(store, reader, client=_c5c_client(args))
|
|
plan = dry.plan_commit(args.sha)
|
|
print(json.dumps(plan, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_live_dry_run(args: argparse.Namespace) -> int:
|
|
"""Read-only Live-Dry-Run gegen realen Forgejo + Tolaria Stand.
|
|
|
|
Erzeugt einen Propagation Plan (WOULD_WRITE | ALREADY_AT_TARGET |
|
|
HUMAN_REVIEW | DRIFT | LEGACY_SPECIAL). Fuehrt KEINE produktive
|
|
Propagation aus. KEINE Writes.
|
|
"""
|
|
store = _store(args.db)
|
|
reader = GitReader(args.repo)
|
|
dry = C5CDryRun(store, reader, client=_c5c_client(args))
|
|
commits = store.list_commits()
|
|
report = {
|
|
"TOTAL_OBJECTS_CHECKED": 0,
|
|
"ALREADY_AT_TARGET": 0,
|
|
"WOULD_WRITE": 0,
|
|
"UNEXPECTED_DRIFT": 0,
|
|
"LEGACY_SPECIAL": 0,
|
|
"HUMAN_REVIEW": 0,
|
|
"commits": [],
|
|
}
|
|
for c in commits:
|
|
sha = c.get("commit_sha") or c.get("sha")
|
|
if not sha:
|
|
continue
|
|
plan = dry.plan_commit(sha)
|
|
for item in plan.get("plan", []):
|
|
report["TOTAL_OBJECTS_CHECKED"] += 1
|
|
p = item.get("plan")
|
|
if p == "ALREADY_AT_TARGET":
|
|
report["ALREADY_AT_TARGET"] += 1
|
|
elif p == "WOULD_WRITE":
|
|
report["WOULD_WRITE"] += 1
|
|
elif p == "DRIFT":
|
|
report["UNEXPECTED_DRIFT"] += 1
|
|
elif p == "LEGACY_SPECIAL":
|
|
report["LEGACY_SPECIAL"] += 1
|
|
else:
|
|
report["HUMAN_REVIEW"] += 1
|
|
report["commits"].append({"commit_sha": sha, "plan": plan.get("plan", [])})
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5c_guarantees(args: argparse.Namespace) -> int:
|
|
"""Prueft No-Search- und No-Master-Write-Guarantee (statisch)."""
|
|
search = assert_no_search_calls()
|
|
master = assert_no_master_write()
|
|
result = {"no_search_calls": search, "no_master_write": master}
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
ok = search["no_search_calls"] and master["no_master_write"]
|
|
return 0 if ok else 1
|
|
|
|
|
|
def _c5d_search_client(args: argparse.Namespace) -> SearchClient:
|
|
return SearchClient(base_url=args.search_base, token=args.search_token)
|
|
|
|
|
|
def cmd_apply_commit(args: argparse.Namespace) -> int:
|
|
"""Vervollstaendigt einen Commit: Search-Rebuild -> Health/Integrity -> APPLIED.
|
|
|
|
NUR aus ST_UPDATING_SEARCH (Tolaria verifiziert) oder ST_RETRY_PENDING
|
|
(Search-Retry-Replay). Fuehrt KEINEN Tolaria-Write aus.
|
|
"""
|
|
store = _store(args.db)
|
|
engine = C5DEngine(store, search=_c5d_search_client(args))
|
|
result = engine.apply_commit(args.sha)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0 if result.get("status") == ST_APPLIED else 1
|
|
|
|
|
|
def cmd_c5d_plan(args: argparse.Namespace) -> int:
|
|
"""Read-only C5D-Plan: Commits in UPDATING_SEARCH / RETRY_PENDING anzeigen."""
|
|
store = _store(args.db)
|
|
dry = C5DDryRun(store)
|
|
print(json.dumps(dry.plan(), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5d_guarantees(args: argparse.Namespace) -> int:
|
|
"""Prueft C5D-Guarantees (statisch): No-Tolaria-Write, No-Master-Write, No-Production."""
|
|
tol = assert_no_tolaria_write()
|
|
master = assert_no_master_write()
|
|
prod = assert_no_production_activation()
|
|
result = {"no_tolaria_write": tol, "no_master_write": master,
|
|
"no_production_activation": prod}
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
ok = tol["no_tolaria_write"] and master["no_master_write"] and prod["no_production_activation"]
|
|
return 0 if ok else 1
|
|
|
|
|
|
# --- C5E-Befehle -----------------------------------------------------------
|
|
|
|
def cmd_c5e_recover(args: argparse.Namespace) -> int:
|
|
"""Recovery-Entscheidung fuer einen Commit (read-only, basierend auf persistiertem State)."""
|
|
store = _c5e_store(args.db)
|
|
engine = C5EEngine(store)
|
|
print(json.dumps(engine.recover(args.sha), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_replay(args: argparse.Namespace) -> int:
|
|
"""Replay/Recovery fuer einen Commit. FAIL-CLOSED: ohne --allow-writes + injizierte
|
|
Engine wird KEIN produktiver Write ausgefuehrt (Standard)."""
|
|
store = _c5e_store(args.db)
|
|
engine = C5EEngine(store, allow_writes=args.allow_writes)
|
|
result = engine.replay(args.sha)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_reconcile(args: argparse.Namespace) -> int:
|
|
"""Read-only Reconciliation: Forgejo Master <-> Tolaria <-> Search (Diagnose, KEIN Repair)."""
|
|
store = _c5e_store(args.db)
|
|
reader = GitReader(args.repo)
|
|
recon = C5EReconciler(
|
|
store,
|
|
reader=reader,
|
|
tol_client=_c5c_client(args),
|
|
search_client=_c5d_search_client(args),
|
|
)
|
|
print(json.dumps(recon.reconcile(), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_observability(args: argparse.Namespace) -> int:
|
|
"""Observability-Felder anzeigen (metadata-minimal, KEINE Knowledge-Inhalte/Secrets)."""
|
|
store = _c5e_store(args.db)
|
|
print(json.dumps(observability(store), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_health(args: argparse.Namespace) -> int:
|
|
"""Ehrlicher Health-Contract (C5E §10): HEALTHY / DEGRADED / BLOCKED."""
|
|
store = _c5e_store(args.db)
|
|
print(json.dumps(health_contract(store), ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_evidence(args: argparse.Namespace) -> int:
|
|
"""Fehler-Evidence fuer einen Commit (C5E §11): commit_sha, object_id, state, reason_code, ..."""
|
|
store = _c5e_store(args.db)
|
|
print(json.dumps(failure_evidence(args.sha, store, object_id=args.object_id),
|
|
ensure_ascii=False, indent=2))
|
|
store.close()
|
|
return 0
|
|
|
|
|
|
def cmd_c5e_guarantees(args: argparse.Namespace) -> int:
|
|
"""Prueft C5E-Guarantees (statisch): No-Production-Activation (fail-closed)."""
|
|
import rq_c5e
|
|
src = open(rq_c5e.__file__, "r").read()
|
|
# Produktive Aktivierung ist nur mit allow_writes=True + injizierter Engine moeglich.
|
|
# Statische Pruefung: Default-Pfad ist fail-closed; kein poll/daemon/forever.
|
|
# Echte Code-Zuweisung `allow_writes=True` als DEFAULT wuerde fail-closed brechen.
|
|
banned = ["def poll(", "while True", "threading.Thread", "schedule.every"]
|
|
flagged = [b for b in banned if b in src]
|
|
# Pruefe, ob es eine echte Default-Zuweisung `allow_writes=True` im
|
|
# Funktions-Signatur-Kontext gibt (nicht nur Docstring-Erwaehnung).
|
|
signature_default_writes = "allow_writes: bool = True" in src
|
|
result = {
|
|
"no_production_activation": True,
|
|
"fail_closed_default": "allow_writes: bool = False" in src,
|
|
"signature_default_writes_true": signature_default_writes,
|
|
"no_polling_daemon": not any(b in src for b in banned),
|
|
"banned_patterns_found": flagged,
|
|
}
|
|
ok = result["fail_closed_default"] and not signature_default_writes \
|
|
and result["no_polling_daemon"]
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0 if ok else 1
|
|
|
|
|
|
# -- C5 DELETE-Execution (Human-Gated) --------------------------------------
|
|
|
|
def _delete_executor(args: argparse.Namespace) -> DeleteExecutor:
|
|
store = _store(args.db)
|
|
client = TolariaClient(base_url=args.tolaria_base)
|
|
return DeleteExecutor(store, client)
|
|
|
|
|
|
def cmd_c5_delete_approve(args: argparse.Namespace) -> int:
|
|
"""
|
|
Persistiert eine explizite Human-Approval fuer einen DELETE-ObjectChange.
|
|
|
|
NUR dieser Befehl erzeugt eine Approval (Human Gate). Er fuehrt KEINEN
|
|
DELETE aus. Der Human-Gate-Beweis (persistierte Approval) geht dadurch
|
|
nie verloren (Mission §3: kein undifferenzierter approve+delete-Blackbox).
|
|
"""
|
|
store = _store(args.db)
|
|
try:
|
|
# ObjectChange muss existieren und operation == DELETE sein.
|
|
changes = store.list_object_changes(args.commit)
|
|
deletes = [c for c in changes if c.get("operation") == OP_DELETE_REQUEST]
|
|
if len(deletes) != 1:
|
|
print(json.dumps({
|
|
"error": f"Erwarte genau einen DELETE-ObjectChange in {args.commit}, "
|
|
f"gefunden: {len(deletes)}",
|
|
"reason_code": "DELETE_APPROVAL_MISMATCH",
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
change = deletes[0]
|
|
oid = change.get("object_id")
|
|
path = change.get("path_before") or change.get("path_after")
|
|
if oid is None or path is None:
|
|
print(json.dumps({
|
|
"error": f"DELETE-ObjectChange {change.get('id')} hat keine object_id/path",
|
|
"reason_code": "DELETE_APPROVAL_MISMATCH",
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
# Optional: --object-id / --path zur Bestaetigung (muss exakt passen).
|
|
if args.object_id and args.object_id != oid:
|
|
print(json.dumps({
|
|
"error": f"--object-id {args.object_id} passt nicht zum ObjectChange {oid}",
|
|
"reason_code": "DELETE_APPROVAL_MISMATCH",
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
if args.path and args.path != path:
|
|
print(json.dumps({
|
|
"error": f"--path {args.path} passt nicht zum ObjectChange {path}",
|
|
"reason_code": "DELETE_APPROVAL_MISMATCH",
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
approval = store.create_delete_approval(
|
|
workflow_commit=args.commit,
|
|
object_change_id=change["id"],
|
|
object_id=oid,
|
|
path=path,
|
|
approved_by=args.approved_by,
|
|
)
|
|
# Commit -> ST_DELETE_APPROVED (nur via expliziter Approval)
|
|
store.transition_commit(args.commit, ST_DELETE_APPROVED)
|
|
print(json.dumps({
|
|
"status": "APPROVED",
|
|
"approval_id": approval["approval_id"],
|
|
"workflow_commit": args.commit,
|
|
"object_change_id": change["id"],
|
|
"object_id": oid,
|
|
"path": path,
|
|
"approved_by": args.approved_by,
|
|
"note": "Approval persistiert. DELETE wird NICHT automatisch ausgefuehrt.",
|
|
}, ensure_ascii=False, indent=2))
|
|
return 0
|
|
except Exception as e: # noqa: BLE001 - CLI-Fehlerbehandlung
|
|
print(json.dumps({"error": str(e)}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
def cmd_c5_delete_execute(args: argparse.Namespace) -> int:
|
|
"""Fuehrt den kontrollierten Tolaria-DELETE aus (verlangt persistierte Approval)."""
|
|
store = _store(args.db)
|
|
executor = DeleteExecutor(store, TolariaClient(base_url=args.tolaria_base))
|
|
try:
|
|
result = executor.execute(args.commit)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0
|
|
except (DeleteApprovalError, DeleteExecutionError) as e:
|
|
print(json.dumps({
|
|
"error": e.message,
|
|
"reason_code": e.reason_code,
|
|
"fail_closed": True,
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
def cmd_c5_delete_status(args: argparse.Namespace) -> int:
|
|
"""Read-only Status: Commit + zugehoerige Approvals."""
|
|
store = _store(args.db)
|
|
try:
|
|
commit = store.get_commit(args.commit)
|
|
approvals = store.list_delete_approvals(args.commit)
|
|
print(json.dumps({
|
|
"commit": commit,
|
|
"approvals": approvals,
|
|
}, ensure_ascii=False, indent=2))
|
|
return 0
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
def cmd_c5_delete_replay(args: argparse.Namespace) -> int:
|
|
"""Idempotenter Replay nach Crash (verlangt persistierte Approval)."""
|
|
store = _store(args.db)
|
|
executor = DeleteExecutor(store, TolariaClient(base_url=args.tolaria_base))
|
|
try:
|
|
result = executor.replay(args.commit)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0
|
|
except (DeleteApprovalError, DeleteExecutionError) as e:
|
|
print(json.dumps({
|
|
"error": e.message,
|
|
"reason_code": e.reason_code,
|
|
"fail_closed": True,
|
|
}, ensure_ascii=False, indent=2))
|
|
return 1
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="rq_c5_cli",
|
|
description="Red Queen C5 CLI (C5A + C5B) — kontrollierte Inspektion & Dry-Run",
|
|
)
|
|
parser.add_argument(
|
|
"--db", default=os.environ.get("C5A_DB", "c5a.db"),
|
|
help="Pfad zur C5A-SQLite-DB (Env C5A_DB, Default c5a.db)",
|
|
)
|
|
parser.add_argument(
|
|
"--repo", default=os.environ.get("C5_REPO", "."),
|
|
help="Pfad zum Forgejo-Clone (Env C5_REPO, Default .)",
|
|
)
|
|
parser.add_argument(
|
|
"--interval", type=int, default=None,
|
|
help=f"Poll-Intervall in Sekunden (Env {ENV_POLL_INTERVAL}, Default {DEFAULT_POLL_INTERVAL_SECONDS})",
|
|
)
|
|
parser.add_argument(
|
|
"--tolaria-base", default=os.environ.get(ENV_TOLARIA_BASE, DEFAULT_TOLARIA_BASE),
|
|
help=f"Tolaria-Vault-API-Basis (Env {ENV_TOLARIA_BASE}, Default {DEFAULT_TOLARIA_BASE})",
|
|
)
|
|
parser.add_argument(
|
|
"--search-base", default=os.environ.get(ENV_SEARCH_BASE, DEFAULT_SEARCH_BASE),
|
|
help=f"Search-API-Basis (Env {ENV_SEARCH_BASE}, Default {DEFAULT_SEARCH_BASE})",
|
|
)
|
|
parser.add_argument(
|
|
"--search-token", default=os.environ.get(ENV_SEARCH_TOKEN, ""),
|
|
help=f"Search-Rebuild-Token (Env {ENV_SEARCH_TOKEN})",
|
|
)
|
|
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p = sub.add_parser("poll-once", help="Ein kontrollierter Poll-Zyklus (read-only)")
|
|
p.set_defaults(func=cmd_poll_once)
|
|
|
|
p = sub.add_parser("show-pending", help="Ausstehende Commits anzeigen")
|
|
p.set_defaults(func=cmd_show_pending)
|
|
|
|
p = sub.add_parser("show-commit", help="Details eines Commits anzeigen")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_show_commit)
|
|
|
|
p = sub.add_parser("dry-run-diff", help="Diff eines Commits anzeigen (read-only)")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_dry_run_diff)
|
|
|
|
p = sub.add_parser("health", help="Health-Contract anzeigen")
|
|
p.set_defaults(func=cmd_health)
|
|
|
|
p = sub.add_parser("no-write-check", help="No-Downstream-Write-Guarantee pruefen")
|
|
p.set_defaults(func=cmd_no_write_check)
|
|
|
|
p = sub.add_parser("propagate-plan", help="Propagation-Plan fuer einen Commit (read-only, KEIN Write)")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_propagate_plan)
|
|
|
|
p = sub.add_parser("live-dry-run", help="Read-only Live-Dry-Run gegen realen Forgejo + Tolaria Stand")
|
|
p.set_defaults(func=cmd_live_dry_run)
|
|
|
|
p = sub.add_parser("c5c-guarantees", help="No-Search- und No-Master-Write-Guarantee pruefen (statisch)")
|
|
p.set_defaults(func=cmd_c5c_guarantees)
|
|
|
|
p = sub.add_parser("apply-commit", help="Commit vervollstaendigen: Search-Rebuild -> Health/Integrity -> APPLIED")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_apply_commit)
|
|
|
|
p = sub.add_parser("c5d-plan", help="Read-only C5D-Plan: Commits in UPDATING_SEARCH / RETRY_PENDING")
|
|
p.set_defaults(func=cmd_c5d_plan)
|
|
|
|
p = sub.add_parser("c5d-guarantees", help="C5D-Guarantees pruefen (statisch): No-Tolaria-Write, No-Master-Write, No-Production")
|
|
p.set_defaults(func=cmd_c5d_guarantees)
|
|
|
|
# --- C5E ---
|
|
p = sub.add_parser("c5e-recover", help="C5E: Recovery-Entscheidung fuer einen Commit (read-only)")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_c5e_recover)
|
|
|
|
p = sub.add_parser("c5e-replay", help="C5E: Replay/Recovery fuer einen Commit (fail-closed, kein Write ohne --allow-writes)")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.add_argument("--allow-writes", action="store_true",
|
|
help="NUR im Test-/Canary-Scope: produktive Writes erlauben (braucht injizierte Engine)")
|
|
p.set_defaults(func=cmd_c5e_replay)
|
|
|
|
p = sub.add_parser("c5e-reconcile", help="C5E: Read-only Reconciliation (Forgejo<->Tolaria<->Search, KEIN Repair)")
|
|
p.set_defaults(func=cmd_c5e_reconcile)
|
|
|
|
p = sub.add_parser("c5e-observability", help="C5E: Observability-Felder anzeigen (metadata-minimal)")
|
|
p.set_defaults(func=cmd_c5e_observability)
|
|
|
|
p = sub.add_parser("c5e-health", help="C5E: Ehrlicher Health-Contract (HEALTHY/DEGRADED/BLOCKED)")
|
|
p.set_defaults(func=cmd_c5e_health)
|
|
|
|
p = sub.add_parser("c5e-evidence", help="C5E: Fehler-Evidence fuer einen Commit (C5E §11)")
|
|
p.add_argument("sha", help="Commit-SHA")
|
|
p.add_argument("--object-id", default=None, help="Optional: Objekt-ID")
|
|
p.set_defaults(func=cmd_c5e_evidence)
|
|
|
|
p = sub.add_parser("c5e-guarantees", help="C5E-Guarantees pruefen (statisch): No-Production-Activation (fail-closed)")
|
|
p.set_defaults(func=cmd_c5e_guarantees)
|
|
|
|
# --- C5 DELETE-Execution (Human-Gated) ---
|
|
p = sub.add_parser("c5-delete-approve",
|
|
help="Persistiert eine explizite Human-Approval fuer einen DELETE-ObjectChange (NUR Approval, KEIN DELETE)")
|
|
p.add_argument("commit", help="Commit-SHA")
|
|
p.add_argument("--object-id", default=None, help="Optional: Objekt-ID zur Bestaetigung")
|
|
p.add_argument("--path", default=None, help="Optional: Vault-Pfad zur Bestaetigung")
|
|
p.add_argument("--approved-by", default="human:christian", help="Human-Gate-Provenance")
|
|
p.set_defaults(func=cmd_c5_delete_approve)
|
|
|
|
p = sub.add_parser("c5-delete-execute",
|
|
help="Fuehrt den kontrollierten Tolaria-DELETE aus (verlangt persistierte Approval)")
|
|
p.add_argument("commit", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_c5_delete_execute)
|
|
|
|
p = sub.add_parser("c5-delete-status", help="Read-only Status: Commit + Approvals")
|
|
p.add_argument("commit", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_c5_delete_status)
|
|
|
|
p = sub.add_parser("c5-delete-replay",
|
|
help="Idempotenter Replay nach Crash (verlangt persistierte Approval)")
|
|
p.add_argument("commit", help="Commit-SHA")
|
|
p.set_defaults(func=cmd_c5_delete_replay)
|
|
|
|
args = parser.parse_args(argv)
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|