- 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)
298 lines
14 KiB
Markdown
298 lines
14 KiB
Markdown
# C5 DELETE EXECUTION CONTRACT — ARCHITECTURE DECISION RECORD
|
||
|
||
**Status:** ACCEPTED (Christian, OPTION 1 — CONTRACT-ERWEITERUNG, 2026-08-26)
|
||
**Scope:** C5 Sync Service — kontrollierter Tolaria-DELETE-Pfad NACH expliziter Human-Freigabe
|
||
**Datei:** `tolaria/c5-sync-service/C5_DELETE_EXECUTION_ARCHITECTURE_DECISION.md`
|
||
|
||
---
|
||
|
||
## 1. Ausgangslage / DELETE_EXECUTION_CONTRACT_GAP
|
||
|
||
STRICT PRE-FLIGHT (read-only) hat den bestehenden DELETE-Contract vollständig analysiert
|
||
(`rq_c5a.py`, `rq_c5b.py`, `rq_c5c.py`, `rq_c5d.py`, `rq_c5e.py`, `rq_c5_cli.py`,
|
||
`C5AStore`, `TolariaClient`, Tests C5A–C5E). Ergebnis:
|
||
|
||
| Owner | Verantwortlich | Status |
|
||
|---|---|---|
|
||
| DELETE_DETECTION_OWNER | C5B `ChangeClassifier.classify()` (Z.520–523): `path_before && !path_after` → `OP_DELETE_REQUEST` | vorhanden |
|
||
| DELETE_HUMAN_GATE_OWNER | C5A `SyncStateMachine.process_commit()` (Z.794–797) + C5C `_propagate_object()` (Z.492–496) → `ST_HUMAN_REVIEW_REQUIRED` + `RC_AMBIGUOUS_DELETE` | vorhanden (terminal) |
|
||
| DELETE_APPROVAL_STORAGE | **fehlt** — C5AStore hat keine Approval-Repräsentation (nur `meta`, `commits`, `objects`, `bootstrap`, `health`) | GAP |
|
||
| DELETE_EXECUTION_OWNER | **fehlt** — `TolariaClient` hat keine `delete()`; kein Executor | GAP |
|
||
| DELETE_VERIFICATION_OWNER | **fehlt für DELETE** — `verify()` nur für Writes (Read-Back+Hash); kein DELETE-absent-Read-Back | GAP |
|
||
| DELETE_REPLAY_OWNER | C5E `recover()`/`replay()`: `ST_HUMAN_REVIEW_REQUIRED` → `REC_HUMAN_REVIEW` (kein Executor) | GAP |
|
||
| DELETE_APPLIED_OWNER | C5D `apply_commit()` → `ST_APPLIED`+`mark_applied()`, startet nur aus `ST_UPDATING_SEARCH` — DELETE-Commit erreicht das nie | GAP |
|
||
|
||
**Kernbefund:** DELETE wird erkannt und zum Human Gate geführt, aber das Gate ist **terminal**.
|
||
Es existiert kein Approval-Storage, kein Executor, kein DELETE-Read-Back, kein Replay- und
|
||
kein APPLIED-Pfad. Dies ist der `DELETE_EXECUTION_CONTRACT_GAP`, der durch diese Mission
|
||
sauber geschlossen wird.
|
||
|
||
---
|
||
|
||
## 2. ZIELARCHITEKTUR (verbindlich)
|
||
|
||
```
|
||
DELETE erkannt (C5B)
|
||
→ HUMAN_REVIEW_REQUIRED (C5A/C5C, RC_AMBIGUOUS_DELETE) [bleibt erhalten]
|
||
→ explizite Human Approval (CLI approve-delete) [NEU]
|
||
→ Approval persistent/auditierbar (C5AStore) [NEU]
|
||
→ kontrollierter Tolaria DELETE (DeleteExecutor) [NEU]
|
||
→ Read-Back-Verifikation (target absent) [NEU]
|
||
→ C5D Source Build (bestehender Pfad) [wiederverwendet]
|
||
→ Search Update (bestehender C5D-Pfad) [wiederverwendet]
|
||
→ Exact-Set-Verifikation (C5D verify_integrity) [wiederverwendet]
|
||
→ APPLIED (C5D mark_applied) [wiederverwendet]
|
||
```
|
||
|
||
**KEIN automatischer DELETE.** Der bestehende Sicherheitsmechanismus
|
||
`OP_DELETE_REQUEST → HUMAN_REVIEW_REQUIRED → RC_AMBIGUOUS_DELETE` bleibt unverändert.
|
||
|
||
---
|
||
|
||
## 3. Zwingende Invarianten (A–N) und ihre Umsetzung
|
||
|
||
| Invariante | Umsetzung |
|
||
|---|---|
|
||
| **A) DELETE bleibt HUMAN_GATED** | Kein Pfad führt einen DELETE ohne persistierte, explizite Human-Approval aus. `_HUMAN_GATE_OPS` unverändert. |
|
||
| **B) Kein LLM/Agent entscheidet, dass DELETE genehmigt ist** | Approval wird AUSSCHLIESSLICH durch den CLI-Befehl `approve-delete` erzeugt (Human Gate). Kein Code erzeugt Approval automatisch. |
|
||
| **C) Approval stammt explizit vom Human Gate** | `approve-delete` persistiert `approved_by` (Human-Gate-Provenance) + `approval_nonce`. |
|
||
| **D) Approval commit- und objectchange-spezifisch** | Approval bindet exakt `(workflow_commit, object_change_id, object_id, path)`. Keine globale `DELETE_APPROVED=true`-Semantik. |
|
||
| **E) Approval für Objekt A nie Objekt B** | Executor validiert `object_id` + `path` gegen die Approval. Abweichung → FAIL CLOSED. |
|
||
| **F) Approval für Commit X nie DELETE aus Commit Y** | Approval bindet `workflow_commit`. Executor validiert Commit-SHA. |
|
||
| **G) Approval persistent und crash-/restart-fest** | SQLite-Tabelle `delete_approvals` in C5AStore (atomic, WAL, restart-safe). |
|
||
| **H) Execution idempotent** | Pre-Delete-Drift-Check + Read-Back. Wenn Ziel bereits absent → `DELETE_ALREADY_AT_TARGET`, kein zweiter destruktiver Write. |
|
||
| **I) DELETE nur exakt freigegebenes Objekt** | Executor nimmt `path`/`object_id` ausschließlich aus dem ObjectChange, das durch die Approval gebunden ist. |
|
||
| **J) Kein Pfad nimmt beliebige freie Paths/IDs ohne ObjectChange+Approval** | Executor verlangt existierendes ObjectChange (`operation==DELETE`) + exakt passende Approval. Kein freier Pfad-Parameter. |
|
||
| **K) Tolaria DELETE vor und nach Execution verifiziert** | Pre-Delete-Drift-Check (Objekt existiert noch) + Post-Read-Back (target absent + kein anderes Objekt verändert). |
|
||
| **L) Keine manuelle SQLite-/SQL-State-Manipulation** | Nur Store-Methoden. Kein direktes SQL im Executor. |
|
||
| **M) Keine direkte APPLIED-Markierung** | APPLIED nur durch C5D `apply_commit()` nach vollem Search-PASS. Executor markiert NIE APPLIED. |
|
||
| **N) Keine automatische Human-Gate-Deaktivierung** | `_HUMAN_GATE_OPS` unverändert; DELETE bleibt Gate-pflichtig. |
|
||
|
||
---
|
||
|
||
## 4. APPROVAL CONTRACT
|
||
|
||
**Prüfung bestehender State:** C5AStore besitzt KEINE geeignete persistente
|
||
Approval-Repräsentation (nur `meta`-KV für source_provenance, `commits`, `objects`,
|
||
`bootstrap`, `health`). → **Neuer minimaler auditierbarer Approval-State.**
|
||
|
||
### 4.1 Schema (neue Tabelle `delete_approvals` in C5AStore)
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS delete_approvals (
|
||
approval_id TEXT PRIMARY KEY, -- UUID, eindeutig
|
||
workflow_commit TEXT NOT NULL, -- Commit-SHA (Invariante F)
|
||
object_change_id INTEGER NOT NULL, -- FK auf objects.id (Invariante D/E)
|
||
object_id TEXT NOT NULL, -- exakt freigegebenes Objekt (E/I)
|
||
path TEXT NOT NULL, -- exakter Vault-Pfad (E/I)
|
||
operation TEXT NOT NULL DEFAULT 'DELETE',
|
||
approval_status TEXT NOT NULL, -- APPROVED | USED | REVOKED
|
||
approved_by TEXT NOT NULL, -- Human-Gate-Provenance (C)
|
||
approval_nonce TEXT NOT NULL, -- eindeutig, verhindert Reuse
|
||
approved_at INTEGER NOT NULL,
|
||
used_at INTEGER,
|
||
UNIQUE(workflow_commit, object_change_id, object_id, path)
|
||
)
|
||
```
|
||
|
||
**Keine Credentials, keine personenbezogenen Daten.** `approved_by` = Human-Gate-Identifier
|
||
(z.B. `human:christian`), kein Secret.
|
||
|
||
### 4.2 Keying
|
||
|
||
Approval ist eindeutig gebunden an `(workflow_commit, object_change_id, object_id, path)`.
|
||
Keine globale `DELETE_APPROVED=true`-Semantik. Ein Approval autorisiert GENAU EINEN
|
||
ObjectChange in GENAU EINEM Commit.
|
||
|
||
### 4.3 CLI-Modell (kein undifferenzierter approve+delete-Blackbox)
|
||
|
||
Bevorzugtes Modell (Mission §3):
|
||
1. `c5-delete approve --commit <sha> --object-id <id> --path <path>` → persistiert Approval (NUR Approval, KEIN DELETE)
|
||
2. `c5-delete execute --commit <sha>` → führt DELETE aus (verlangt persistierte Approval)
|
||
3. `c5-delete status --commit <sha>` → read-only Status
|
||
4. `c5-delete replay --commit <sha>` → idempotenter Replay nach Crash
|
||
|
||
Der Human-Gate-Beweis (persistierte Approval) geht dadurch NIE verloren.
|
||
|
||
---
|
||
|
||
## 5. TOLARIA DELETE CLIENT
|
||
|
||
`TolariaClient.delete(vault_path)` — minimal, isoliert:
|
||
|
||
```python
|
||
def delete(self, vault_path: str) -> Dict[str, Any]:
|
||
"""Loescht ein Vault-Objekt (POST /delete). NUR DeleteExecutor darf rufen."""
|
||
resp = self._post("delete", {"path": vault_path})
|
||
if resp is None:
|
||
resp = {}
|
||
if "error" in resp:
|
||
raise TolariaWriteError(f"Tolaria delete fehlgeschlagen: {resp['error']}",
|
||
RC_INVALID_SCHEMA)
|
||
return resp
|
||
```
|
||
|
||
**Endpoint-/Payload-Contract** wird ausschließlich aus dem vorhandenen Tolaria-Code/Router
|
||
und der dokumentierten Incident-Evidence bestimmt (POST `/api/vault/delete` mit
|
||
`{"path": ...}`). **KEINE produktive Endpoint-Probe.** Die Incident-Regel gilt verbindlich:
|
||
`NO MUTATING HTTP METHOD PROBES DURING PRODUCTION READ-ONLY DISCOVERY`.
|
||
|
||
---
|
||
|
||
## 6. DELETE EXECUTION (DeleteExecutor)
|
||
|
||
### 6.1 Pre-Delete-Gates (alle MÜSSEN passen, sonst FAIL CLOSED)
|
||
|
||
1. ObjectChange existiert (`get_object_change`)
|
||
2. `operation == DELETE`
|
||
3. Commit-Status == `ST_DELETE_APPROVED` (definierter approved state)
|
||
4. Approval existiert, `approval_status == APPROVED`, gehört exakt zu diesem ObjectChange
|
||
5. `object_id` stimmt (Approval ↔ ObjectChange)
|
||
6. `path` stimmt (Approval ↔ ObjectChange)
|
||
7. Pre-Delete-Drift-Check: Tolaria enthält das Objekt noch (erwarteter aktueller Zustand)
|
||
|
||
Bei irgendeiner Abweichung → **FAIL CLOSED** (kein DELETE, kein State-Übergang).
|
||
|
||
### 6.2 Execution
|
||
|
||
- `client.delete(vault_path)` (nur DeleteExecutor)
|
||
- **Read-Back:** `client.read(vault_path) is None` → target absent == TRUE
|
||
- **Zusätzlich:** `client.list()` — kein anderes Objekt verändert (Set-Vergleich vor/nach)
|
||
|
||
### 6.3 Nach erfolgreichem DELETE
|
||
|
||
- Approval → `USED` (einmalig, verhindert Reuse)
|
||
- Commit → `ST_UPDATING_SEARCH` (C5D übernimmt Search-Pfad)
|
||
|
||
---
|
||
|
||
## 7. IDEMPOTENZ / CRASH-RECOVERY (Fälle A–F)
|
||
|
||
| Fall | Zustand | Deterministischer Replay-Pfad |
|
||
|---|---|---|
|
||
| **A) Crash vor Approval** | `ST_HUMAN_REVIEW_REQUIRED`, keine Approval | Nichts zu tun. Kein DELETE. Replay: bleibt Human Gate. |
|
||
| **B) Crash nach Approval, vor DELETE** | `ST_DELETE_APPROVED`, Approval persistiert | Replay: Executor erkennt Approval, führt DELETE aus. |
|
||
| **C) Crash während DELETE** | `ST_DELETING` | Replay: Read-Back. Objekt noch da → DELETE erneut (Approval gültig). Objekt absent → `DELETE_ALREADY_AT_TARGET`, weiter zu `ST_UPDATING_SEARCH`. |
|
||
| **D) Crash nach DELETE, vor Read-Back** | `ST_DELETING` | Replay: Read-Back → absent → weiter zu `ST_UPDATING_SEARCH`. |
|
||
| **E) Crash nach Read-Back, vor Search Update** | `ST_UPDATING_SEARCH` | C5D `apply_commit()` übernimmt (bestehender Pfad). |
|
||
| **F) Crash nach Search Update, vor APPLIED** | `ST_VERIFYING_SEARCH` | C5D `apply_commit()` übernimmt (bestehender Pfad). |
|
||
|
||
**DELETE_ALREADY_AT_TARGET:** Wenn Tolaria das freigegebene Objekt bereits nicht mehr
|
||
enthält, erkennt der Executor anhand Approval + erwarteter Identität + Read-Back
|
||
`DELETE_ALREADY_AT_TARGET` statt blind erneut zu mutieren.
|
||
|
||
---
|
||
|
||
## 8. SEARCH CONTRACT
|
||
|
||
Nach erfolgreichem Tolaria-DELETE wird Search NICHT über einen statischen/improvisierten
|
||
Pfad aktualisiert. Der bestehende C5D-Pfad wird wiederverwendet:
|
||
|
||
```
|
||
Tolaria current verified state (ohne gelöschtes Objekt)
|
||
→ SearchSourceBuilder.build() (read-only, deterministisch)
|
||
→ atomic source + Provenance-Persistenz (persist_search_source_provenance)
|
||
→ Search-Rebuild (search.rebuild)
|
||
→ Exact-Set-Verifikation (verify_integrity: expected IDs/Paths exakt)
|
||
→ APPLIED (mark_applied)
|
||
```
|
||
|
||
**Kein APPLIED bei Search-Mismatch** (C5D `_handle_failure` → Human Gate / DEAD).
|
||
|
||
---
|
||
|
||
## 9. STATE MACHINE CHANGES
|
||
|
||
Neue Zustände (minimal, nur für den DELETE-Execution-Pfad):
|
||
|
||
- `ST_DELETE_APPROVED = "DELETE_APPROVED"` — nach Approval, vor Execution
|
||
- `ST_DELETING = "DELETING"` — während Execution (Crash-Window)
|
||
|
||
Neue erlaubte Transitionen (NUR durch DeleteExecutor/Approval auslösbar, nie automatisch):
|
||
|
||
```
|
||
(ST_HUMAN_REVIEW_REQUIRED, ST_DELETE_APPROVED) # nur via expliziter Approval
|
||
(ST_DELETE_APPROVED, ST_DELETING) # Executor startet
|
||
(ST_DELETING, ST_UPDATING_SEARCH) # nach DELETE + Read-Back → C5D
|
||
(ST_DELETING, ST_HUMAN_REVIEW_REQUIRED) # Fehler → zurück zum Gate
|
||
(ST_DELETE_APPROVED, ST_HUMAN_REVIEW_REQUIRED) # Revoke/Fehler
|
||
```
|
||
|
||
**Keine Änderung** an `_HUMAN_GATE_OPS`, `RC_AMBIGUOUS_DELETE`, oder den bestehenden
|
||
Transitions. Der bestehende `OP_DELETE_REQUEST → HUMAN_REVIEW_REQUIRED`-Pfad bleibt
|
||
unverändert.
|
||
|
||
---
|
||
|
||
## 10. CLI CHANGES
|
||
|
||
Neue Befehle in `rq_c5_cli.py`:
|
||
|
||
- `c5-delete approve --commit <sha> --object-id <id> --path <path>` → persistiert Approval (NUR Approval)
|
||
- `c5-delete execute --commit <sha>` → führt DELETE aus (verlangt persistierte Approval)
|
||
- `c5-delete status --commit <sha>` → read-only Status
|
||
- `c5-delete replay --commit <sha>` → idempotenter Replay nach Crash
|
||
|
||
**Kein undifferenzierter approve+delete-Blackbox-Befehl.**
|
||
|
||
---
|
||
|
||
## 11. SECURITY REVIEW (Design-Zusicherungen)
|
||
|
||
- **Keine arbitrary path deletion:** Executor nimmt `path` nur aus dem ObjectChange, das durch Approval gebunden ist.
|
||
- **Keine path traversal:** `path` wird gegen `VAULT_PREFIX` validiert.
|
||
- **Keine ungeprüften freien Delete-Targets:** Kein freier Pfad-Parameter im Executor.
|
||
- **Keine Approval-Reuse:** `approval_id` eindeutig; nach Nutzung → `USED`.
|
||
- **Keine Cross-Commit-Approval:** Approval bindet `workflow_commit`.
|
||
- **Keine Cross-Object-Approval:** Approval bindet `object_id` + `path`.
|
||
- **Keine automatische Approval-Eskalation:** Nur expliziter CLI-Befehl erzeugt Approval.
|
||
- **Keine Secrets:** Approval speichert keine Credentials/personenbezogenen Daten.
|
||
- **Keine Credential-Ausgabe:** Kein Secret im Approval-/Status-Output.
|
||
- **Keine manuelle DB-Manipulation:** Nur Store-Methoden.
|
||
- **Keine allgemeine DELETE-Automatisierung:** Kein Pfad führt DELETE ohne Approval.
|
||
|
||
---
|
||
|
||
## 12. TESTPLAN (mindestens)
|
||
|
||
- DELETE ohne Human Approval → BLOCKED
|
||
- DELETE mit Approval für anderes Objekt → BLOCKED
|
||
- DELETE mit Approval für anderen Commit → BLOCKED
|
||
- manipulierte object_id → BLOCKED
|
||
- manipulierter path → BLOCKED
|
||
- gültiges Approval → exakt ein DELETE
|
||
- anderes Objekt bleibt unverändert
|
||
- DELETE_ALREADY_AT_TARGET → kein zweiter destruktiver Write
|
||
- Restart nach Approval → Approval erhalten
|
||
- Crash nach DELETE → Replay sicher
|
||
- Search Exact-Set nach DELETE
|
||
- Search-Mismatch → kein APPLIED
|
||
- Secret Safety
|
||
- keine SQL-/State-Bypässe
|
||
- bestehende WRITE-/UPDATE-Flows ohne Regression
|
||
- HUMAN_REVIEW_REQUIRED bleibt Default für neue DELETE requests
|
||
- keine automatische Approval-Erzeugung
|
||
- Multi-Commit-Test: Approval A darf DELETE B niemals autorisieren
|
||
|
||
---
|
||
|
||
## 13. INCIDENT LESSON (verbindlich, bleibt)
|
||
|
||
```
|
||
INCIDENT_OCCURRED = TRUE
|
||
INCIDENT_RECOVERED = TRUE
|
||
PERMANENT_DAMAGE = NONE VERIFIED
|
||
ROOT_CAUSE = unsafe mutating endpoint probe during read-only discovery
|
||
```
|
||
|
||
**Neue verbindliche Regel:** `NO MUTATING HTTP METHOD PROBES DURING PRODUCTION READ-ONLY
|
||
DISCOVERY`. Diese Regel darf durch die neue DELETE-Implementierung NICHT aufgeweicht werden.
|
||
|
||
---
|
||
|
||
## 14. HARD STOP
|
||
|
||
Nach erfolgreicher Implementation, Tests, Push und Fresh Checker: **STOPP.**
|
||
KEIN realer Canary-Delete. KEIN produktiver Search-Rebuild. KEIN C5F-Cleanup. KEIN C5G.
|
||
KEINE Netzwerkänderung. KEINE Hermes-Rechte. KEINE Hermes-Autonomie.
|
||
Christian erteilt danach separat die Freigabe zur produktiven Canary-Cleanup-Validierung.
|