MH-3b ProposalWriter — Plan-Review

Projekt: Steve-TradingBot · Phase: RECON-MH-3b · Author: claude-opus-4-7[1m]
Generated: 2026-05-12 17:47 UTC · master HEAD: 26c8ab3 (MH-3a closed)
Status: NO CODE Plan-Review only — Operator-GO erforderlich vor MH-3b Code-Phase


1 — Live-State Verification

CheckSollIstOK
master HEAD26c8ab326c8ab3 mh-3a: add dry-run managed proposal engine v1
git statuscleanempty output
Bot-Containerrunningclawbot Up 35h healthy
Bot in-container PID2998429984 python3 main.py --paper
Worker-Containerrunningclawbot-worker Up 18h (unhealthy) — Healthcheck-Drift, kein Funktionsproblem
BINANCE_TESTNETtrueBINANCE_TESTNET=true
runtime_config.jsonabsentnot present
baseline_holdings.jsonabsentnot present
managed_state.jsonabsentnot present
state/risk_proposals/absentnot present
Tracebacks last 300 lines00 matches

2 — MH-3a-Konsumiertes Surface

KomponenteStatusBedeutung für MH-3b
RiskProposalEngine V1-Minimalcommitted 26c8ab3Engine bleibt Source-of-Truth für payload-Format
dry_run=True returns ProposalResultWriter kann result.payload direkt konsumieren
dry_run=False raises NotImplementedError("ProposalWriter not available until MH-3b")Aktivierungsfrage für MH-3b: bleibt diese Meldung oder wird sie gelifted? Siehe §5
risk_model_version="phase1-min-v1" / proposal_version=1pinnedWriter-Validator prüft Anwesenheit (G-DR-10)
confidence.overall_score PflichtenforcedEngine garantiert; Writer dupliziert defensive Check
proposal_id als UUID-String erzeugtvia uuid_factoryWriter validiert Filename-Safe-Pattern
STABLE_PEG_BLOCKLIST / MainnetBlockedErrorEngine raises vor ComputeWriter eigene Mainnet-Defense-Layer (Defense-in-Depth)

Pattern-Referenz: trading/baseline_holdings_writer.py (600 LOC, RECON-2.1). Behält:

Unterschiede zum Baseline-Pattern:

3 — ProposalWriter Scope-Definition

3.1 — Target Layout

state/risk_proposals/
  └── <proposal_id>.json     ← eine Datei pro Engine-Output, immutable
EigenschaftWert
Default target_dirDEFAULT_STATE_DIR / "risk_proposals" = <trading>/state/risk_proposals/
Filename-Pattern<proposal_id>.json (UUID-String + .json)
In Container/home/node/.openclaw/workspace/trading/state/risk_proposals/<id>.json (passt zu ProposalReader.DEFAULT_PROPOSALS_DIR)

3.2 — proposal_id Validierung (anti-path-traversal)

Validator-Regex (vorgeschlagen): ^[a-zA-Z0-9_-]{1,128}$ (akzeptiert UUID4 default + Engine custom-IDs).

RejectReason
"" / " " (empty/whitespace)invalid
"../etc/passwd" (path traversal)contains / und ..
"a/b" (path traversal)contains /
"a\\b" (path traversal Windows)contains \\
".hidden" (leading dot)filesystem-hidden semantics
"file\\x00trick" (null byte)injection
"this-is-129-chars-long-..." > 128 charsbound length
"id with space"non-canonical

Defense-in-Depth: nach Regex-Pass zusätzlich Path(filename).name == filename Check (PathLib normalisiert path; Identity-Check fängt Edge-Cases).

3.3 — Atomic Write Sequenz

  1. Validate payload-dict shape (raises before IO):
  2. Mainnet defense layer (Defense-in-Depth, unabhängig von Engine):
  3. Ensure target_dir exists: os.makedirs(target_dir, exist_ok=True) (auto-create OK — keine Mutation existierender Files)
  4. Collision check: if target_path.exists() → return ApplyResult(ok=False, error="proposal_id_collision"). Kein Overwrite, kein Backup-Move.
  5. Atomic write:
  6. sha256 berechnen für ApplyResult (audit-relevant; analog Baseline-Writer)
  7. Return ApplyResult(ok=True, target_path, sha256, written_at)

3.4 — Detailfragen

FrageAntwort
Temp-file + rename?Ja — tempfile.mkstemp in same dir + os.replace. Pattern aus Baseline-Writer übernommen.
fsync nötig?Ja — f.fsync() nach json.dump. Schützt gegen Crash zwischen Write und Power-Loss. Directory-fsync optional (Baseline-Writer macht's nicht, wir matchen).
Permissions?Default umask. 0644 für JSON, 0755 für Dir. Kein chmod explicit — Files sind nicht secret (kein Token-Material), Audit-Trail. Stricter chmod ist BACKLOG (operator-optional).
Backup bei overwrite?Nein — keine Overwrite-Möglichkeit (Immutable per spec).
Darf overwrite vorkommen?Nein. Writer refuses mit proposal_id_collision Error.
canonical_hash nötig?Nein für Filing-Idempotency (proposal_id ist unique key). sha256 des geschriebenen Body ja für Audit-Result.
created_at / written_at metadata?written_at wird im ApplyResult zurückgegeben (separate Spur vom generated_at im Payload). Kein in-payload-mutation — payload bleibt 1:1 wie Engine es liefert (immutable).

4 — Boundary-Contract

4.1 — Forbidden Imports (AST-pinned)

main · command_worker · live_trade · paper_trade · risk_manager

proposal_engine darf NICHT importiert werden (Writer ↛ Engine — sonst Zyklus). Writer akzeptiert plain dict-payload.

4.2 — Forbidden Source-Tokens (Grep-pinned)

4.3 — Was Writer NICHT macht

TabuBegründung
managed_state.json schreibenMH-4 Scope
baseline_holdings.json schreibenRECON-2.1 hat eigenen Writer
risk_proposals/*.json löschen / archivierenMH-9 TTL-cleanup-Job
DB-InsertMH-6 Worker-Handler
audit_events InsertMH-6 Worker-Handler
command_worker.py touchenMH-6
main.py / live_trade.py / paper_trade.py touchenMH-7
Bot-WiringMH-7
Exchange/API writeaus Prinzip — Writer ist filesystem-pure
MainnetALLOWED_ENVIRONMENTS = {"testnet"} + MainnetBlockedError raise

5 — Integration mit MH-3a

Option A — Engine unverändert, Writer standalone (empfohlen)

Option B — Engine bekommt optional writer-Konstruktor-Param

Option C — Engine ungenagelt, neue Compose-Methode in Writer

Empfehlung: Option A

  1. Klare Trennung von Computation vs. Persistence
  2. Engine MH-3a closure bleibt zementiert
  3. MH-6 worker handler ist die natürliche Orchestrierungs-Stelle
  4. Tests für „dry_run=True schreibt nichts" bleiben byte-identisch — keine Re-Run-Diskussion

Dry-Run-Persistenz-Guarantee Test (Option A):

6 — Test-Plan

Test-KlasseAnzahlInhalt
ProposalWriterAtomicWriteTests4tempfile + replace; partial-write-on-crash recoverable; sha256 stable; round-trip Reader→Writer
ProposalWriterDirCreationTests2absent dir auto-create; existing dir reused
ProposalIdValidationTests8empty / whitespace / .. / / / \\ / leading dot / null byte / 129-chars rejected
ProposalWriterCollisionTests3existing file refuses overwrite; refused even on identical content; refused with sha256-different content
PayloadValidationTests8proposal_id mismatch; missing proposal_version; wrong proposal_version; missing risk_model_version; missing confidence.overall_score; score out of range; missing asset; missing generated_at
MainnetBlockedTests3BINANCE_TESTNET=False raises; missing setting raises; raised before any file IO
EnvironmentAllowlistTests2only testnet ALLOWED_ENVIRONMENTS pinned; future-mainnet refused
ApplyResultTests4ok=True populates target_path + sha256; ok=False populates error; frozen dataclass; written_at ISO-8601 UTC
RestoreFromBackupTests3optional helper for symmetry with Baseline-Writer; tests existence + no-raise contract
ReaderRoundTripTests3Writer.apply(payload) → ProposalReader.read(id) yields equivalent ProposalSnapshot; identity check via raw-payload-comparison
IntegrationTests (Option A)2end-to-end: Engine.generate(dry_run=True) → Writer.apply(result.payload); Mock-Writer test pins dry_run=True never calls writer
BoundaryASTTests5forbidden imports (5 modules); no order API in source; no foreign state file refs; no DB tokens; no proposal_engine import
SourceGrepTests4no .write_text outside atomic helper; no os.unlink (proposals immutable); no shutil.copy of risk_proposals (no backups); no path concat with ..
NoRealNetworkTests1sanity — Writer doesn't import network modules
Total~52 Tests

7 — Betroffene Dateien (Final-Inventory)

DateiStatusPhase
trading/proposal_writer.pyNEUMH-3b
trading/tests/test_proposal_writer.pyNEUMH-3b
trading/proposal_engine.pyunverändertMH-3a closure intact
trading/tests/test_proposal_engine.pyunverändertMH-3a tests intact
trading/proposal_reader.pyunverändertMH-2 reader intact
trading/tests/test_proposal_reader.pyunverändertMH-2 tests intact
trading/baseline_holdings_writer.pyunverändertRECON-2.1 reference (pattern source)
0 PHP touches
0 DB-Migration
0 docker cp
0 Bot/Worker restart

Erwartete Größenordnung: ~350-450 LOC Writer + ~700-850 LOC Tests = ~1100-1300 LOC total (analog MH-3a 1307 LOC).

8 — Stop-Regeln MH-3b

IDStop wenn…
MH-3b-SR-1Writer schreibt eine andere Datei als state/risk_proposals/<proposal_id>.json
MH-3b-SR-2Writer überschreibt eine bestehende Proposal-Datei
MH-3b-SR-3Writer löscht / verschiebt eine bestehende Proposal-Datei
MH-3b-SR-4Writer schreibt managed_state.json oder baseline_holdings.json oder runtime_config.json
MH-3b-SR-5Writer emittet INSERT INTO commands oder INSERT INTO audit_events
MH-3b-SR-6Writer akzeptiert proposal_id mit /, \\, .., NUL-Byte, > 128 chars, oder leading dot
MH-3b-SR-7Writer schreibt mit Settings.BINANCE_TESTNET != True (Mainnet-Block-Layer-Bypass)
MH-3b-SR-8Writer ruft ccxt-API auf (create_*, fetch_*, cancel_*, etc.)
MH-3b-SR-9AST-Boundary-Test schlägt fehl (Import von main / command_worker / live_trade / paper_trade / risk_manager / proposal_engine)
MH-3b-SR-10Tests treffen echtes Filesystem außerhalb von tempfile.mkdtemp Sandbox
MH-3b-SR-11Atomic-Write-Test schlägt fehl (tmpfile sichtbar nach erfolgreichem replace)
MH-3b-SR-12Reader-Round-Trip-Test schlägt fehl (Writer-Output nicht von ProposalReader.read() parsbar)

9 — Backup / Restart-Bedarf

AktionPflicht?
pg_dump GUI-DBNEIN — kein DB-Touch
live_portfolio.json snapshotNEIN
.env snapshotNEIN
state/ snapshotNEIN — neue Files only, kein Mutation an existierenden
Health-SnapNEIN
Memory-SicherungNEIN — Closure-Pin reicht
Bot-RestartNEIN — kein Wiring, kein Bot-Code-Touch
Worker-RestartNEIN — Worker-Handler ist MH-6
docker cpNEIN

MH-3b ist Code+Test+Commit auf master. Analog MH-2 / MH-3a.

10 — Identifizierte Risiken

#RisikoSeverityMitigation
R1Filesystem-Atomarität nicht garantiert wenn target_dir auf anderem Filesystem als TMPDIRtempfile.mkstemp mit dir=target_dir löst das (gleiche FS)LOWOverride dir= explicit auf target_dir; identisches Pattern zu Baseline-Writer
R2proposal_id collision — UUID4 hat 2^122 Entropie, Kollision praktisch unmöglich, aber theoretisch möglichLOWCollision-Check vor Write + klarer Error; Tests pinnen das Verhalten
R3Concurrent writes mit gleicher proposal_id — race condition: zwei Caller validieren not exists, beide schreibenMEDIUMtempfile.mkstemp hat O_CREAT \| O_EXCL; os.replace ist atomic. Pragmatisch: in MH-3a sind nur 1 Worker-Pfad, single-writer, kein Concurrency-Risiko in Praxis. Test mit Mock-Concurrency dokumentiert Vorsicht.
R4JSON-Serialisierungs-Fehler bei unbekannten Datentypen im PayloadLOWEngine erzeugt nur JSON-safe Werte (str/int/float/dict/list/bool). Writer prüft json.dump Exception → Failure-Path mit klarer Meldung
R5Path-Traversal-Bypass via Unicode — z.B. RTL-Override-Zeichen, normalized vs raw NFCMEDIUMStrikte ASCII-Regex [a-zA-Z0-9_-]{1,128}; alle non-ASCII reject. + Path(filename).name == filename als zweite Schicht
R6Disk-Space-Erschöpfung beim WriteLOWTempfile cleanup im except-Branch; OS raise OSError; tests können das mit Mock-Filesystem prüfen
R7fsync Performance auf Container-Storage — write+fsync kann auf Bind-Mounts langsam seinLOWAkzeptabel: Engine-Output erfolgt selten (Operator-getriggered, nicht per Cycle). Performance-Tests nicht in Scope MH-3b
R8Pattern-Mirror-Drift vom Baseline-Writer — wenn Baseline-Writer später ein Bug-Fix bekommt, Proposal-Writer driftetLOWPattern-Reference-Comment im Doctring; CI-Lint-Rule wäre BACKLOG (MH-3b-FU-1)

11 — GO/NO-GO-Empfehlung

Empfehlung: GO Option A — Engine bleibt unverändert, ProposalWriter standalone.

AspektBewertung
Scope-Größemedium (~1100-1300 LOC code+tests, ~52 Tests)
KomplexitätPattern-Mirror Baseline-Writer; gut bekannte Atomic-Write-Semantik; keine State-Interdependenz
Blast-RadiusNULL — keine existierenden Files modifiziert; Engine MH-3a closure bleibt byte-identisch
Roll-Back-Costminimal: git revert <commit> + rm -rf state/risk_proposals/ (kein DB / runtime touch)
Dependencies-Klärungenkeine — alle Pflicht-Schemas/Felder bereits in MH-3a Engine-Output
Risiko-BewertungLOW
Backup-Pflichtnein
Restart-Pflichtnein
docker cp Pflichtnein

Pre-Implementation-Q an Operator

  1. Option A oder B?Empfehlung A (Engine untouched, max scope-lock)
  2. proposal_id-Regex-Schwere: strict ASCII [a-zA-Z0-9_-]{1,128} oder erlaubt z.B. dots in der Mitte (für proposal_id-Namespacing wie org.eth.20260512)? ← Empfehlung strict ASCII no-dots, einfacher pin
  3. fsync Directory zusätzlich zum file-fsync? ← Empfehlung nein (matches Baseline-Writer)
  4. restore_from_backup Helper mit liefern (Symmetrie zu Baseline-Writer) obwohl proposals immutable sind und nie restored werden müssten? ← Empfehlung weglassen (YAGNI)
  5. MH-3c Real-Testnet-Drill als separate Phase oder erst Teil von MH-6? ← Empfehlung MH-6 (Drill braucht Worker-Handler-Pfad, der Engine+Writer orchestriert)

Subschnitt

PhaseInhaltScope
MH-3b (jetzt)proposal_writer.py + Tests, Engine untouchedatomic
MH-3c (später)Real-Testnet-Drill — empirisch Engine+Writer gegen echtes Testnetmanual operator-drill
MH-4 (später)ManagedStateWriter + Two-File-Atomic Promote/ReleaseG-DR-14
MH-5 (später)Filament Multi-Step-WizardUX-2
MH-6 (später)Worker-Handler für managed.* Commands (Engine + Writer + DB-Cache + audit_events)orchestration
MH-7 (später)Bot-Wiring (universe / balance_provider / execute_buy / drift-detection)Restart-Pflicht

STOP vor Implementierung. Erwarte Operator-GO mit Option-A/B Wahl + Pre-Implementation-Q1-Q5-Approval.

© Steve-TradingBot · RECON-MH-3b · Plan-Review (no-code phase)