MH-4b-2 ProposalService::createApproveCommand — Plan-Review

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


1 — Live-State Verification

CheckSollIstOK
master HEAD3fd78463fd7846 mh-4b-1: add proposal request command service
git statuscleanempty output
Bot in-container PID2998429984 python3 main.py --paper
Worker Host PID338185python3 -m trading.command_worker running
cmd 13cancelled13 | apply_baseline_holdings | cancelled
managed_proposals rows00
managed_assets_history rows00
BINANCE_TESTNETtrueBINANCE_TESTNET=true
runtime_config.jsonabsentnot present
baseline_holdings.jsonabsentnot present
managed_state.jsonabsentnot present
Tracebacks last 200 lines00 matches

2 — Konsumierte Artefakte

KomponenteStatusKonsumiert in MH-4b-2
MH-4a: managed_proposals + managed_assets_history Tables + Enum + Models83dafcaREAD-only auf managed_proposals für State-Validation
MH-4b-1: ProposalService::createRequestCommand3fd7846Service-Skelett-Pattern + AuditMetadataScrubber-Helper bleiben
MH-1: approve_managed_proposal CommandType-Stubea11637$registry->get('approve_managed_proposal') Existence-Check
MH-3a: Engine V1 + CONFIDENCE_THRESHOLD=0.5026c8ab3Service-Konstante CONFIDENCE_THRESHOLD=0.50 (parity-mirror)
MH-3b: ProposalWriter immutable62a08f4Service nutzt proposal_sha256 + proposal_file_path aus managed_proposals als Integrity-Snapshot im approve-Payload
MH-2: ProposalReader50cc5c2Worker-only — Service nicht angefasst
ManagedProposalState::RiskProposed (Enum)83dafcaState-Validation nutzt das Enum
ProposalService::createRejectCommandMH-4b-3nicht in MH-4b-2
Worker-HandlerMH-6nicht in MH-4b-2
Filament WizardMH-5nicht in MH-4b-2

Pattern-Source: gui/app/Services/Apply/Baseline/ApplyBaselineService::createApplyCommand (Idempotency-Pre-Check, DB::transaction, writeAudit) + gui/app/Services/Apply/Baseline/BaselineHoldingsAllowlist::buildHardConfirmString (Hard-Confirm-Pattern <count>:<sha8>).

3 — MH-4b-2 Scope-Definition

Approve-Lebenszyklus-Stelle

Operator-Wizard (MH-5)
  ↓ submit final step
ProposalService::createApproveCommand()                ← MH-4b-2
  ↓
DB::transaction:
   - READ managed_proposals row (validation)
   - READ existing commands with idempotency-key (decide-pattern)
   - INSERT commands (status='pending', payload, key)
   - INSERT audit_events (managed.asset_promote_requested)
  ↓
==================== Service boundary ====================
  ↓
Worker --once / Daemon                                  ← MH-6
  ↓
_handle_approve_managed_proposal:
   - re-validate proposal_sha256 against on-disk file
   - Two-File-Atomic (managed_state.json + baseline_holdings.json)
   - UPDATE managed_proposals SET state='managed_active', decided_at=...
   - INSERT managed_assets_history
   - audit managed.asset_promoted

Welche States dürfen approved werden?

Erlaubt:

Verboten (jeweils mit klarer Exception):

StateReasonException
frozenAsset noch nicht analysiertProposalAlreadyDecidedException (detail="state=frozen, no proposal exists")
proposal_pendingWorker hat Engine noch nicht laufen lassenProposalNotCachedException
proposal_rejectedbereits abgelehntProposalAlreadyDecidedException
proposal_abortedEngine hat abort liefert (z.B. no_balance)ProposalAlreadyDecidedException
managed_active / managed_paused / managed_drift_alert / managed_released / exit_executedbereits promoted / past-LebenszyklusProposalAlreadyDecidedException

Plus die DB-Cache-Integrity-Checks:

ConditionException
managed_proposals row not foundProposalNotFoundException (5. Exception)
proposal_json IS NULLProposalNotCachedException
proposal_sha256 IS NULLProposalNotCachedException
proposal_file_path IS NULLProposalNotCachedException
decided_at IS NOT NULLProposalAlreadyDecidedException

proposal_sha256 + proposal_file_path Behandlung

Service liest beide aus managed_proposals row und echoed sie in den approve-Payload als Integrity-Snapshot:

{
  "proposal_sha256":    "sha256:abc...",
  "proposal_file_path": "/home/node/.openclaw/.../risk_proposals/<id>.json"
}

Vorteile:

proposal_id Validierung

Strict-ASCII Regex ^[a-zA-Z0-9_-]{1,128}$ (1:1 mirror MH-3b PROPOSAL_ID_REGEX). Service-internes private const + Parity-Test (analog ManagedProposalStateEnumTest).

Hard-Confirm-Format

Operator-pinned in MH-4b Plan-Review:

Confidence-Override-Logik

score = managed_proposals.proposal_json.confidence.overall_score
threshold = 0.50  // CONFIDENCE_THRESHOLD, parity mit MH-3a engine

if score < threshold AND confidenceOverride !== true:
    throw ConfidenceOverrideRequiredException(score, threshold)

if score < threshold AND confidenceOverride === true:
    payload['confidence_override_used'] = true
    audit.metadata['confidence_override_used'] = true
    // weiter mit approve

if score >= threshold:
    payload['confidence_override_used'] = false
    // confidenceOverride wird ignoriert (kein Effekt)

Idempotency-Pattern (Shared-Key mh:decide:<pid>)

SzenarioVerhalten
1st approve, no existing commandINSERT new approve command + audit; return new
2nd approve (double-click), existing approvereturn existing (idempotency hit, NO 2nd audit)
approve, existing reject command for same pidthrow ProposalAlreadyDecidedException(detail='rejected')
approve, existing approve in succeeded statereturn existing (idempotent)

Wichtig: Service muss existing->command_type prüfen, NICHT nur Existence. Bei Mismatch → explicit exception (kein silent return des falschen Typs).

Was wird gelesen vs. geschrieben

READ (SELECT):

WRITE (INSERT):

NEVER WRITE/UPDATE:

4 — Strict Boundary

TabuPin
managed_proposals INSERT/UPDATESource-Grep: ManagedProposal::create / ->save() / ->update() verboten; ManagedProposal::find($pid) und ->where() erlaubt
managed_assets_history WriteSource-Grep: ManagedAssetHistory::* verboten
managed_state.json / baseline_holdings.json / risk_proposals/*.json WriteFile-IO-Call-Tokens (file_put_contents(, fwrite(, etc.) Pin
ProposalWriter-Aufrufarchitektonisch unmöglich (PHP↔Python); plus Source-Grep
command_worker.py Touchgit-diff nur gui/ files
Bot-Code Touchgit-diff nur gui/ files
Filament-UIMH-5 — nur Service + Tests + Exceptions
MainnetService-Konstante APPROVE_ENVIRONMENT = 'testnet' hardcoded

5 — Sonder-Anforderungen geprüft

FrageAntwort
Idempotency mh:decide:<pid> shared mit rejectPattern. Service::createApproveCommand prüft Existence + Command-Type. Bei reject-mismatch: ProposalAlreadyDecidedException.
Existierender reject-commandProposalAlreadyDecidedException(detail='rejected')
Existierender approve-command→ return existing (idempotency hit; NO 2nd audit)
audit_event prefix managed.*Service nutzt event_type='managed.asset_promote_requested'; Test: assertStartsWith('managed.', ...)
Transaction rollbackDB::transaction-Wrap; Test: simulate AuditEvent::create failure → command rollback verified
Hard-Confirm gegen falsche Proposal-IDOperator gibt <asset>:<variant> ein, NICHT proposal_id. Service vergleicht gegen managed_proposals.asset + ':' + $variant. Falsche asset/variant-Kombi → HardConfirmMismatchException.
confidence.overall_score und override_requiredsiehe §3 Confidence-Override-Logik; 3 Pfade pinned
Mainnet/testnet enforcement(a) Service hardcodes environment='testnet'; (b) CommandTypeRegistry allowedEnvironments=['testnet']; (c) Worker-Handler MH-6 prüft zusätzlich; (d) Engine MH-3a MainnetBlockedError; (e) Writer MH-3b MainnetBlockedError. → 5-Layer-Block intakt

6 — Empfohlene Lieferung

Service-API

ProposalService::createApproveCommand(
    string  $proposalId,         // ^[a-zA-Z0-9_-]{1,128}$
    int     $userId,             // > 0
    string  $variant,            // ∈ {recommended, conservative, aggressive}
    array   $overrides,          // optional, whitelist [stop_loss, take_profit, max_allocation_usdt, trailing_stop_enabled]
    string  $hardConfirm,        // exact match "<asset>:<variant>"
    bool    $confidenceOverride = false,
): Command

ProposalService::buildHardConfirmString(string $asset, string $variant): string
    // returns "<asset>:<variant>"

Erlaubte Service-Methoden

Verbotene Service-Methoden

Exception-Liste

Operator hatte 4 Exceptions spezifiziert. Empfehlung: 5 Exceptions für maximale Caller-Klarheit:

#ExceptionWhenRecovery
1ProposalNotFoundExceptionrow mit proposalId existiert nicht in managed_proposalsOperator-Action: zurück zum Wizard, neuer Request
2ProposalAlreadyDecidedExceptionstate ∉ risk_proposed OR decided_at IS NOT NULL OR existing reject commandOperator-Action: GUI zeigt aktuellen State; "already decided as X"
3ProposalNotCachedExceptionstate=proposal_pending OR proposal_json/sha256/file_path IS NULL (Worker hat noch nicht gelaufen)Operator-Action: warten + retry; alternativ Worker manuell --once
4HardConfirmMismatchExceptionhardConfirm ≠ <asset>:<variant>Operator-Action: re-type confirm-string
5ConfidenceOverrideRequiredExceptionscore < 0.50 und confidenceOverride=falseOperator-Action: confidenceOverride=true setzen ODER reject

Alternative (4 Exceptions, operator-original): ProposalNotFound wird in ProposalAlreadyDecidedException gefoldet mit detail='not_found'. → Operator entscheidet.

Payload-Schema (commands.payload_json)

{
  "environment":                "testnet",
  "proposal_id":                "<pid>",
  "asset":                      "<asset>",
  "variant":                    "recommended|conservative|aggressive",
  "overrides":                  { /* optional whitelisted keys */ },
  "hard_confirm":               "<asset>:<variant>",
  "confidence_override_used":   false,
  "confidence_score_at_decide": 0.65,
  "proposal_sha256":            "sha256:abc...",
  "proposal_file_path":         "/home/node/.../<pid>.json",
  "decided_by":                 <userId>,
  "expires_at":                 "<iso8601-z>"
}

overrides Whitelist:

7 — Test-Plan

Test-KlasseAnzahlInhalt
ProposalServiceCreateApproveTest (happy path)6INSERT commands + audit · payload env=testnet · payload alle required keys · audit prefix managed.* · hardConfirm exact-match · confidence high-score
ProposalServiceCreateApproveStateTests8reject when state=frozen · proposal_pending · proposal_rejected · proposal_aborted · managed_active · managed_paused · managed_drift_alert · managed_released · exit_executed
ProposalServiceCreateApproveNotFoundTest2proposal_id not in DB → ProposalNotFoundException · empty managed_proposals table
ProposalServiceCreateApproveCacheTests4proposal_json IS NULL · proposal_sha256 IS NULL · proposal_file_path IS NULL · all 3 NULL
ProposalServiceCreateApproveDecidedTests2decided_at IS NOT NULL · existing reject command (shared-key)
ProposalServiceCreateApproveHardConfirmTests6exact match · asset mismatch · variant mismatch · case mismatch (eth:recommended) · empty string · invalid format (no colon)
ProposalServiceCreateApproveConfidenceTests5high-score happy (override ignored) · low-score-no-override → exception · low-score-with-override → happy + audit-flag · score=0.50 (boundary) · missing confidence.overall_score → exception
ProposalServiceCreateApproveVariantTests4recommended · conservative · aggressive · invalid variant string
ProposalServiceCreateApproveOverridesTests7empty overrides · only stop_loss · only take_profit · all 4 whitelist keys · reject strategy_group · reject variant · reject custom key
ProposalServiceCreateApproveIdempotencyTests4approve-twice returns existing (no 2nd audit) · approve after reject → ProposalAlreadyDecidedException · idempotency-key format pinned · DB-Unique-Constraint
ProposalServiceCreateApproveAtomicityTest2failed audit insert rolls back command · no orphan
ProposalIdRegexParityTest2PHP regex matches MH-3b char-for-char · accept/reject cases parity
ConfidenceThresholdParityTest1Service::CONFIDENCE_THRESHOLD === 0.50 (mirror MH-3a engine)
BoundaryTests3no managed_proposals UPDATE · no managed_assets_history INSERT · no file-IO/subprocess tokens
HardConfirmBuilderTest3builds <asset>:<variant> · case preserved · usable in Filament
Total~59 Tests(über Roadmap-Schätzung ~30; viele State-Combos)

8 — Stop-Regeln MH-4b-2

IDStop wenn…
MH-4b-2-SR-1Service UPDATE auf managed_proposals (auch nicht Model::update())
MH-4b-2-SR-2Service INSERT in managed_assets_history
MH-4b-2-SR-3Service file IO oder subprocess
MH-4b-2-SR-4Service akzeptiert environment != 'testnet'
MH-4b-2-SR-5Service ruft Worker oder spawnt Subprocess
MH-4b-2-SR-6Service-Methode NICHT in DB::transaction
MH-4b-2-SR-7proposal_id Regex weicht von MH-3b ab
MH-4b-2-SR-8hardConfirm Validierung ist case-insensitive
MH-4b-2-SR-9CONFIDENCE_THRESHOLD weicht von MH-3a 0.50 ab (Parity-Test bricht)
MH-4b-2-SR-10confidence_override_used Flag fehlt in audit-metadata bei override
MH-4b-2-SR-11Audit-Event-Prefix ist nicht managed.*
MH-4b-2-SR-12overrides Whitelist akzeptiert strategy_group / variant / confidence_threshold
MH-4b-2-SR-13Shared-Key reject-Command wird stumm zurückgegeben statt ProposalAlreadyDecidedException
MH-4b-2-SR-14proposal_sha256 / proposal_file_path fehlen im commands.payload (Integrity-Trail)

9 — Backup / Migration / Restart-Bedarf

AktionPflicht?Begründung
pg_dump GUI-DBNEINkeine Migration; reine Service-Code-Phase
live_portfolio.json snapshotNEINkein State-Touch
state/ snapshotNEINkein State-Touch
Memory-SicherungNEINClosure-Pin reicht
DB MigrationNEINMH-4a Schema bleibt unverändert
Bot-RestartNEINkein Bot-Code-Touch
Worker-RestartNEINcommand_worker.py unverändert
docker cpNEINPHP via Laravel Live-Volume mount
GUI Cache-ClearNEIN (per Operator-MH-4b-1-Korrektur)keine Config/Routes/Views-Änderung

MH-4b-2 = Reine Code+Test+Commit Phase.

10 — Risiken-Übersicht

#RisikoSeverityMitigation
R1Service UPDATE auf managed_proposals versehentlichLOWSource-Grep ManagedProposal::create|update|save|delete Test
R2proposal_id Regex Drift PHP↔MH-3bLOWParity-Test gegen hardcoded MH-3b-mirror
R3CONFIDENCE_THRESHOLD Drift PHP↔MH-3aLOWParity-Test prüft === 0.50
R4Hard-Confirm-Bypass via case-mismatchLOWcase-sensitive === + 6 Test-Cases
R5Confidence-Score-Read aus stale DB-Cache (Worker hat ausgelaufenen file aber DB nicht aktualisiert)MEDIUMService liest aus managed_proposals.proposal_json (DB), nicht von Disk. Worker verifies sha256 zur Execute-Zeit. Akzeptable Read-Cache-Staleness im Service-Scope.
R6Approve-Reject-Race ohne Type-CheckMEDIUMIdempotency-Pre-Check prüft existing->command_type und throws ProposalAlreadyDecidedException bei mismatch
R7confidence_override silently true ohne score < thresholdLOWAudit-metadata confidence_override_used ist nur true wenn beides zutrifft. Test pinned.
R8overrides whitelist drift (neue keys ohne G-DR-3 Q-MH-3 Review)LOWWhitelist-Konstante + Test pinnt exakt die 4 erlaubten keys
R9proposal_sha256/file_path staleLOW (MH-6 prüft)Service echoed das DB-Cache-Value; Worker-Handler MH-6 verifies sha256 gegen on-disk Datei zur Execute-Zeit (defense-in-depth)
R10DB::transaction-Rollback bei AuditEvent failureLOWPattern bewährt aus ApplyBaselineService + MH-4b-1; Test simuliert
R11proposal_json deserialization JSON-Cast-DriftLOWEloquent $casts = ['proposal_json' => 'array'] (MH-4a). Test pinned.
R12confidence.overall_score missing in proposal_json (Worker-Bug)LOWService validiert path-existence; throws InvalidCommandPayloadException (subtype)

11 — Kleinste sichere Code-Phase + GO/NO-GO Empfehlung

Subschnitt-Optionen

VarianteInhaltLOCTestsRisk
MH-4b-2-monolithiccreateApproveCommand + 5 Exceptions + 15 Test-Klassen + buildHardConfirmString~1900~59LOW-MEDIUM
MH-4b-2-aNUR Skelett: createApproveCommand mit happy-path + 1 Exception + 1 Test-Klasse~400~12LOW
MH-4b-2-b (folgt)State/Cache/Decided Exceptions (3 Exceptions) + 3 Test-Klassen~500~16LOW
MH-4b-2-c (folgt)Hard-Confirm + Confidence + Overrides + Idempotency + Atomicity + Boundary + Parity~1000~31MEDIUM

Empfehlung: MH-4b-2-monolithic

Begründung dieses Mal MONOLITHISCH (anders als MH-4b-1):

  1. Approve ist atomar in der Logik — alle 5 Exceptions + Hard-Confirm + Confidence + Overrides bauen auf demselben Eingangs-Validation-Stack auf. Sub-Cuts würden den Validations-Stack unvollständig deployen → erhöhtes Bug-Risiko bei intermediate-state.
  2. Pattern bereits etabliertApplyBaselineService::createApplyCommand ist die Vorlage; alle Sub-Mechaniken (DB::transaction, audit, idempotency) sind bewährt. Kein Pattern-Discovery in MH-4b-2.
  3. Test-Vollständigkeit — Approve hat den größten Validation-Stack im gesamten MH-Service-Layer. Sub-Cuts würden Coverage-Lücken hinterlassen (z.B. Hard-Confirm würde in b fehlen wenn a deployed ist).
  4. R5 / R6 / R10 nur durch volles Set abdeckbar — Race-Schutz, Stale-Cache und Atomicity sind kein add-on, sondern integral.
  5. Rollback-Cost minimal — alles in einem Commit git revert-bar; DB hat 0 Rows im managed_proposals → 0 Side-Effect-Risiko.

Falls Operator dennoch Sub-Cut bevorzugtMH-4b-2-a (Skelett + happy-path + ProposalNotFoundException) als Erstcommit ist die sicherste Variante (etablisiert das loadProposal-Helper-Pattern).

Pre-Implementation-Q an Operator

  1. MH-4b-2-monolithic oder MH-4b-2-a (Skelett)?Empfehlung monolithisch
  2. 5 oder 4 Exceptions? ProposalNotFound als 5. eigene Exception, oder in ProposalAlreadyDecidedException gefoldet? ← Empfehlung 5 separate Exceptions (Caller-Klarheit)
  3. Hard-Confirm exakt <asset>:<variant> oder <asset>:<variant>:<sha8>? ← Empfehlung <asset>:<variant> (Operator-Original; sha256 ist im Payload separat)
  4. Overrides-Whitelist: 4 keys (stop_loss / take_profit / max_allocation_usdt / trailing_stop_enabled) — alle akzeptieren? ← Empfehlung JA (Q-MH-3 Operator-Override-Scope)
  5. CONFIDENCE_THRESHOLD = 0.50 als Service-Konstante mit Parity-Test gegen MH-3a — bestätigen? ← Empfehlung JA (Defense-in-Depth durch Parity-Pin)
  6. Audit-event_type: managed.asset_promote_requested (Service-Layer-Event distinct von Worker-Success-Event managed.asset_promoted)? ← Empfehlung JA

Scope-Größe + Risiko-Bewertung MH-4b-2-monolithic

AspektBewertung
LOC~700 Service + ~150 Exceptions + ~1100 Tests = ~1900
Tests~59
KomplexitätPattern-Mirror ApplyBaselineService; größter Validation-Stack im Service-Layer
Blast-RadiusNULL — INSERT-only auf commands + audit_events
Roll-Back-Costminimal: git revert + DB hat 0 neue Rows
Dependencies-KlärungQ-MH-3 Override-Whitelist, CONFIDENCE_THRESHOLD Pin
Backup-PflichtNEIN
Migration-PflichtNEIN
Restart-PflichtNEIN
Mainnet-TouchNEIN
docker cpNEIN

STOP vor Implementierung. Erwarte Operator-GO mit Subschnitt-Wahl (monolithisch empfohlen) + Q1-Q6-Approval.

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