fix(broker): enforce operator broker truth boundary

This commit is contained in:
Hermes Agent
2026-07-15 08:07:21 +09:00
parent 1b63e79edc
commit 216e462df4
19 changed files with 603 additions and 35 deletions
+54 -10
View File
@@ -329,6 +329,29 @@ def _fail(message: str, as_json: bool) -> None:
print(f"error: {message}", file=sys.stderr)
def _broker_read_failure(exc, as_json: bool) -> int:
"""REQ-120 command boundary: expose code only, never upstream payloads."""
code = getattr(exc, "code", "broker_unavailable")
payload = {"error": {"code": code, "message": "broker balance unavailable"}}
if as_json:
print(_json.dumps(payload), file=sys.stderr)
else:
print(f"broker balance unavailable ({code})", file=sys.stderr)
return 1
class _CachedBalanceBroker:
"""One validated balance read per operator invocation; delegates everything else."""
def __init__(self, broker, balance) -> None:
self._broker, self._balance = broker, balance
def get_balance(self):
return self._balance
def __getattr__(self, name):
return getattr(self._broker, name)
# --------------------------------------------------------------------------- #
# arg parsing
# --------------------------------------------------------------------------- #
@@ -446,7 +469,6 @@ def _build_parser() -> argparse.ArgumentParser:
def _cmd_sync(args, cfg, confirm) -> int:
"""REQ-105 (dcy-034, BAT-051): sync slot qty to broker ground truth."""
import psycopg
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
@@ -476,11 +498,18 @@ def _cmd_sync(args, cfg, confirm) -> int:
print("aborted")
return 1
warn_store = PostgreSQLReconcileWarningStore(cfg.dsn)
# REQ-119/120: resolve and validate broker truth before opening/enumerating DB.
from trex_core.runtime.broker_provider import (
BrokerCredentialUnavailable, build_broker_provider,
)
from trex_core.application.broker_port import BrokerBalanceError
try:
broker_port, _source = build_broker_provider(cfg).get_broker_for_read()
broker_port = _CachedBalanceBroker(broker_port, broker_port.get_balance())
except (BrokerCredentialUnavailable, BrokerBalanceError) as exc:
return _broker_read_failure(exc, args.json)
# Build broker port
from trex_core.runtime.daemon import _build_broker
broker_port = _build_broker(cfg)
warn_store = PostgreSQLReconcileWarningStore(cfg.dsn)
# Build slot_repo + slot_service
from trex_core.adapters.persistence.postgresql import PostgreSQLSlotRepository
@@ -498,6 +527,7 @@ def _cmd_sync(args, cfg, confirm) -> int:
)
# Determine target slot_ids
import psycopg
if args.portfolio_id:
with psycopg.connect(cfg.dsn) as conn:
with conn.cursor() as cur:
@@ -564,7 +594,7 @@ def _cmd_sync(args, cfg, confirm) -> int:
print(f" after: status={after.get('status')} "
f"qty={after.get('qty')} avg_price={after.get('avg_price')}")
if cleared:
print(f" reconcile-warning CLEARED")
print(" reconcile-warning CLEARED")
return exit_code
@@ -579,12 +609,20 @@ def _cmd_verify(args, cfg) -> int:
from trex_core.application.reconciliation import verify_slot_reconcile
instance = _resolve_instance(args)
# REQ-120 no-write/no-enumeration barrier also applies to verify.
from trex_core.runtime.broker_provider import (
BrokerCredentialUnavailable, build_broker_provider,
)
from trex_core.application.broker_port import BrokerBalanceError
try:
broker_port, _source = build_broker_provider(cfg).get_broker_for_read()
broker_port = _CachedBalanceBroker(broker_port, broker_port.get_balance())
except (BrokerCredentialUnavailable, BrokerBalanceError) as exc:
return _broker_read_failure(exc, args.json)
warn_store = PostgreSQLReconcileWarningStore(cfg.dsn)
slot_repo = PostgreSQLSlotRepository(cfg.dsn)
from trex_core.runtime.daemon import _build_broker
broker_port = _build_broker(cfg)
# Build a minimal snapshot store (we only need asset_quantity for C-case check).
# For verify we use a fresh SnapshotStore populated from warn_store metadata.
# The hub_qty is read from the snapshot; if snapshot is empty we fall back to 0
@@ -787,7 +825,13 @@ def main(argv: list[str] | None = None, *, rpc=None, confirm=None) -> int:
else None
)
_load_dotenv(_toml, getattr(args, "env_file", None))
cfg = DaemonConfig.from_env()
cfg = (
DaemonConfig.from_config(
resolve_config_toml(args.config, args.config_path), env_file=args.env_file
)
if args.config
else DaemonConfig.from_env()
)
confirm = confirm or _default_confirm
# REQ-088 (dcy-029): operator report — direct-DB point-in-time aggregate.