operator-plane: guarded command bus (flatten / pause kill switch)
Adds OperatorCommandEvent + a closed command vocabulary (flatten_all, flatten_symbol, pause_entries, resume_entries). POST /api/command emits it (token-required: 503 when DASHBOARD_TOKEN unset — commands never run unauthenticated); the live loop's new 5s "command" step consumes them via OperatorCommandConsumer with three replay-safety layers (in-session nonce set, persisted operator_command_done reload, 10-min staleness window). flatten_all routes through close_session DIRECTLY (never step_eod_flatten) so it cannot trip plan 003's daily-restart exit. Pause is checked at the top of BOTH request_approvals_for_ready_signals AND submit_breakout_now (missing either leaks pause on that path). qabot gains an authorized-chat-only /flatten slash command (prose "flatten" still refuses via _ACTION_WORDS). Snapshot exposes broker truth + command_bus + a defensive experiment ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbb84ae166
commit
e7c34f298f
8 files changed
+619
No files matched your search
@@ -227,6 +227,33 @@ def answer(store, question: str, *, host: str, model: str, timeout: int,
|
||||
return f"(LLM unavailable — here's the raw snapshot)\n{context}"
|
||||
|
||||
|
||||
# --------------------------------------------------------- operator kill switch
|
||||
def parse_flatten_command(text: str):
|
||||
"""Parse the ONE action the qabot performs: the explicit ``/flatten`` slash
|
||||
command. Returns ``(command, symbol)`` or None. Prose like "sell everything"
|
||||
is NOT matched here — it still hits the _ACTION_WORDS refusal in answer(). This
|
||||
is the deliberate carve-out: an explicit slash command from the authorized chat
|
||||
is allowed; free-text trade requests are not."""
|
||||
parts = text.strip().split()
|
||||
if not parts or parts[0].lower() != "/flatten":
|
||||
return None
|
||||
if len(parts) >= 2 and parts[1].strip():
|
||||
return ("flatten_symbol", parts[1].strip().upper())
|
||||
return ("flatten_all", None)
|
||||
|
||||
|
||||
def emit_operator_command(store, command: str, symbol, requested_by: str) -> None:
|
||||
"""Emit the same OperatorCommandEvent the dashboard does — the live loop's 5s
|
||||
consumer executes it (single execution authority)."""
|
||||
import uuid
|
||||
from storage.event_schema import EventMode, OperatorCommandEvent
|
||||
store.emit(OperatorCommandEvent(
|
||||
timestamp=datetime.now(), mode=EventMode.PAPER,
|
||||
message=f"telegram operator command {command}" + (f" {symbol}" if symbol else ""),
|
||||
command=command, symbol=symbol, requested_by=requested_by,
|
||||
nonce=uuid.uuid4().hex))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- dead-man
|
||||
def _now_et() -> datetime:
|
||||
"""ET wall clock, tz-NAIVE — events.timestamp is ET-naive (RUNBOOK), so the
|
||||
@@ -369,6 +396,18 @@ def main() -> None:
|
||||
if not is_authorized(chat_id, allowed):
|
||||
logger.warning("ignoring message from unauthorized chat %s", chat_id)
|
||||
continue
|
||||
# explicit /flatten slash command (authorized chat only) — the qabot's
|
||||
# one action. Emits the guarded event; the loop executes it in ~5s.
|
||||
cmd = parse_flatten_command(text)
|
||||
if cmd is not None:
|
||||
try:
|
||||
emit_operator_command(store, cmd[0], cmd[1],
|
||||
requested_by=f"telegram:{chat_id}")
|
||||
_send(token, chat_id, "Command sent — loop executes within ~5s.")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("flatten command emit failed", exc_info=True)
|
||||
_send(token, chat_id, f"(command failed to send: {exc})")
|
||||
continue
|
||||
try:
|
||||
reply = answer(store, text, host=host, model=model, timeout=timeout,
|
||||
ollama_enabled=ollama_enabled)
|
||||
|
||||
Reference in new issue
Block a user