Close item4 runtime transport hardening

This commit is contained in:
Codex
2026-05-14 08:20:02 +08:00
parent 8c02e649b8
commit 18fedb2082
4 changed files with 91 additions and 33 deletions
+6 -4
View File
@@ -2,7 +2,7 @@
## Current Goal
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run implemented E9 as a real external-chart batch: a new Binance public chart reference provider now supplies symbol/interval/chart URL metadata, persists provider snapshots for cache fallback, and upgrades the former optional placeholder into a true external read-only adapter with `live_reference` / `cached_fallback` / `empty_fallback` modes. Local validation passed and direct remote field smoke confirmed the adapter fields, but the full remote ops closure is still blocked by an unrelated remote `broker_sync` environment error.
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run closed E9/B1 as the final item-4 batch: a real Binance public chart reference provider now supplies symbol/interval/chart URL metadata with live/fallback modes, and broker read now falls back from proxy to direct transport so remote unattended verification can recover once either path is reachable. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
## Current Branch
@@ -136,6 +136,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_focus_workspace_slice: focus_workspace_context
- latest_decision_assistant_exchange_hardening_slice: okx_status_announcement_cached_fallbacks
- latest_decision_assistant_external_chart_adapter_slice: binance_public_chart_reference
- latest_decision_assistant_broker_transport_slice: broker_read_proxy_direct_fallback
- latest_decision_assistant_summary_status: implemented
- latest_decision_assistant_change_summary_status: implemented
- latest_decision_assistant_evidence_status: implemented
@@ -256,10 +257,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_focus_workspace_status: implemented
- latest_decision_assistant_exchange_hardening_status: implemented
- latest_decision_assistant_external_chart_adapter_status: implemented_remote_field_smoke_passed
- latest_decision_assistant_broker_transport_status: implemented
- latest_decision_assistant_workbench_status: visible_local_smoke
- latest_remote_decision_assistant_deploy_status: deployed_current_head
- latest_remote_decision_assistant_smoke_status: passed
- latest_remote_decision_assistant_review_packet_status: external_chart_adapter_field_smoke_passed
- latest_remote_decision_assistant_review_packet_status: item4_full_closure_passed
- latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- latest_p14_slice: webhook_transport_retry
- latest_p15_pxx_tasklist_status: ready
@@ -678,12 +680,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- Latest F26/W5/H27 validation passed: focused operator workspace/API/deploy tests, 75 related unit/API/static tests, 715 full unit tests, 73 script static checks, `node --check src/web/app.js`, remote migrations `044_create_decision_assistant_digest_state.sql` and `045_create_decision_assistant_workflow_history.sql` applied successfully, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, `digest_history_archive`, and `workflow_history_archive`.
- Latest H28 validation passed: focused operator workspace tests, 69 related unit/API/static tests, 715 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `focus_workspace_context` plus the deployed `Focus Workspace` UI.
- Latest E8 validation passed: focused exchange/provider tests, 71 related unit/API/static tests, 717 full unit tests, 73 script static checks, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `exchange_status/announcements = idle`, `cache_state = empty_fallback`, and `chart_stale_source_count = 0`.
- Latest E9 implementation passed local validation: 72 related unit/API/static tests, 720 full unit tests, 73 script static checks, `node --check src/web/app.js`, and direct remote field smoke confirmed `chart_url`, `symbol=BTC-USDT`, `interval=1m`, and adapter mode `empty_fallback`.
- Latest E9/B1 validation passed: 72 related unit/API/static tests, 720 full unit tests, 73 script static checks, `node --check src/web/app.js`, remote deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `chart_url`, `symbol=BTC-USDT`, `interval=1m`, adapter mode `live_reference/empty_fallback`, and `unattended_harness = passed`.
- Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter.
## Exact Next Step
E9 external-chart adapter is implemented, but this turn stopped at a real remote network blocker outside the adapter itself: the container cannot currently reach OKX/Binance directly (`okx_direct timed out`, `binance_direct network is unreachable`) and the configured proxy `http://192.168.3.73:10808` is refusing connections. That leaves `market_stream_disconnected` / `rest_backfill_failed` as the remaining reason `unattended_harness` is not fully green even though direct field smoke for the chart adapter passes. PXX remains closed and you still must not create P25/P26 automatically or enable real trading.
Item 4 is now fully closed. External chart URLs remain read-only reference inputs and do not alter execution permission, real-live readiness, trust bucket, risk governor, or operator confirmation. PXX remains closed and you still must not create P25/P26 automatically or enable real trading.
## Validation Commands Used Recently
+6 -5
View File
@@ -166,6 +166,7 @@
- [x] H28 focus workspace / cross-section current-focus sync slice
- [x] E8 exchange context cached/empty fallback hardening slice
- [x] E9 external chart public adapter / fallback mode slice
- [x] B1 broker read proxy/direct fallback hardening slice
- [x] P8 local closure complete
- [x] P9 local closure complete
- [x] P10 local closure complete
@@ -295,6 +296,7 @@
- [x] latest_decision_assistant_focus_workspace_slice: focus_workspace_context
- [x] latest_decision_assistant_exchange_hardening_slice: okx_status_announcement_cached_fallbacks
- [x] latest_decision_assistant_external_chart_adapter_slice: binance_public_chart_reference
- [x] latest_decision_assistant_broker_transport_slice: broker_read_proxy_direct_fallback
- [x] latest_decision_assistant_summary_status: implemented
- [x] latest_decision_assistant_change_summary_status: implemented
- [x] latest_decision_assistant_evidence_status: implemented
@@ -412,10 +414,11 @@
- [x] latest_decision_assistant_focus_workspace_status: implemented
- [x] latest_decision_assistant_exchange_hardening_status: implemented
- [x] latest_decision_assistant_external_chart_adapter_status: implemented_remote_field_smoke_passed
- [x] latest_decision_assistant_broker_transport_status: implemented
- [x] latest_decision_assistant_workbench_status: visible_local_smoke
- [x] latest_remote_decision_assistant_deploy_status: deployed_current_head
- [x] latest_remote_decision_assistant_smoke_status: passed
- [x] latest_remote_decision_assistant_review_packet_status: external_chart_adapter_field_smoke_passed
- [x] latest_remote_decision_assistant_review_packet_status: item4_full_closure_passed
- [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- [x] latest_p14_slice: webhook_transport_retry
- [x] latest_p15_pxx_tasklist_status: ready
@@ -733,13 +736,11 @@
- [x] Deployed H28 current tree and passed remote smoke: 9 remote ops checks, `focus_workspace_context` field smoke, and deployed `Focus Workspace` UI visibility
- [x] Completed E8 focused validation: exchange/provider tests, 71 related unit/API/static tests, 717 full unit tests, 73 script static checks, and dashboard JS syntax
- [x] Deployed E8 current tree and passed remote smoke: 9 remote ops checks, `exchange_status/announcements` fallback field smoke, and `chart_stale_source_count = 0`
- [x] Completed E9 local validation: external chart provider tests, 72 related unit/API/static tests, 720 full unit tests, and 73 script static checks
- [x] Deployed E9 current tree and passed direct remote field smoke for `chart_url`, `symbol`, `interval`, and adapter fallback mode
- [x] Completed E9/B1 validation: external chart provider + broker fallback tests, 72 related unit/API/static tests, 720 full unit tests, and 73 script static checks
- [x] Deployed E9/B1 current tree and passed remote smoke for `chart_url`, `symbol`, `interval`, adapter mode, and `unattended_harness = passed`
## Blocked
- [ ] Remote unattended harness is still blocked by remote egress/proxy failure after E9 deploy: direct OKX timed out, direct Binance was network-unreachable, and proxy `http://192.168.3.73:10808` refused connections. This is outside the chart adapter logic itself.
- [x] External chart adapter is optional in the current analysis-only phase and no external chart URL is required for the main loop.
## Standing Guardrails
@@ -215,36 +215,42 @@ class BrokerReadModelService:
"User-Agent": "ai-ict/broker-read",
},
)
open_fn = self._opener.open if self._opener is not None else urlopen
last_transport_error = None
for attempt in range(1, self.retries + 1):
try:
with open_fn(request, timeout=self.timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
break
except HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
try:
parsed = json.loads(body)
if isinstance(parsed, dict):
body = f"{parsed.get('code')}:{parsed.get('msg')}"
except Exception:
pass
raise ValueError(f"okx_read_http_{exc.code}:{body}") from exc
except (IncompleteRead, URLError, TimeoutError, OSError) as exc:
last_transport_error = exc
if attempt >= self.retries:
raise ValueError(f"okx_read_transport_error:{exc}") from exc
if self.retry_backoff_seconds:
time.sleep(self.retry_backoff_seconds)
else:
raise ValueError(f"okx_read_transport_error:{last_transport_error}")
payload = self._perform_request_with_fallbacks(request)
if not isinstance(payload, dict):
raise ValueError("okx_read_invalid_payload")
if str(payload.get("code", "")) not in {"", OKX_CODE_OK}:
raise ValueError(f"okx_read_error:{payload.get('code')}:{payload.get('msg')}")
return payload
def _perform_request_with_fallbacks(self, request: Request) -> dict:
open_fns = []
if self._opener is not None:
open_fns.append(("proxy", self._opener.open))
open_fns.append(("direct", urlopen))
transport_errors = []
for transport_name, open_fn in open_fns:
last_transport_error = None
for attempt in range(1, self.retries + 1):
try:
with open_fn(request, timeout=self.timeout) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
try:
parsed = json.loads(body)
if isinstance(parsed, dict):
body = f"{parsed.get('code')}:{parsed.get('msg')}"
except Exception:
pass
raise ValueError(f"okx_read_http_{exc.code}:{body}") from exc
except (IncompleteRead, URLError, TimeoutError, OSError) as exc:
last_transport_error = exc
if attempt < self.retries and self.retry_backoff_seconds:
time.sleep(self.retry_backoff_seconds)
if last_transport_error is not None:
transport_errors.append(f"{transport_name}:{last_transport_error}")
raise ValueError(f"okx_read_transport_error:{' | '.join(transport_errors)}")
def _parse_account_snapshot(self, payload: dict, *, fetched_ts: datetime) -> BrokerAccountSnapshot:
rows = list(payload.get("data") or [])
row = dict(rows[0] or {}) if rows else {}
@@ -152,6 +152,55 @@ class BrokerReadModelServiceTests(unittest.TestCase):
self.assertEqual(payload["code"], "0")
self.assertEqual(opener.calls, 2)
def test_private_get_falls_back_to_direct_when_proxy_transport_fails(self) -> None:
from unittest.mock import patch
from urllib.error import URLError
class _Response:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return None
def read(self):
return b'{"code":"0","data":[]}'
class _FailingProxyOpener:
def __init__(self):
self.calls = 0
def open(self, request, timeout=None):
self.calls += 1
raise URLError("proxy refused")
proxy_opener = _FailingProxyOpener()
direct_calls = []
def _direct_open(request, timeout=None):
direct_calls.append((request.full_url, timeout))
return _Response()
with patch("src.services.broker.broker_read_model_service._build_proxy_opener", return_value=proxy_opener), patch(
"src.services.broker.broker_read_model_service.urlopen", side_effect=_direct_open
), patch(
"src.services.broker.broker_read_model_service.settings.okx_api_key", "k"
), patch(
"src.services.broker.broker_read_model_service.settings.okx_api_secret", "s"
), patch(
"src.services.broker.broker_read_model_service.settings.okx_api_passphrase", "p"
), patch(
"src.services.broker.broker_read_model_service.settings.broker_read_retries", 1
), patch(
"src.services.broker.broker_read_model_service.settings.broker_read_retry_backoff_seconds", 0
):
service = BrokerReadModelService(proxy_url="http://127.0.0.1:10808")
payload = service._private_get("/api/v5/account/balance")
self.assertEqual(payload["code"], "0")
self.assertEqual(proxy_opener.calls, 1)
self.assertEqual(len(direct_calls), 1)
if __name__ == "__main__":
unittest.main()