From 4dac80b215ac4476e432c72f7a631ccad512a9ae Mon Sep 17 00:00:00 2001 From: Svrnty Date: Sun, 24 May 2026 12:16:46 -0400 Subject: [PATCH] feat(adwright route): wire real Adwright data via adwright_core import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routes/adwright.py now lazy-imports adwright_core from adwright-mcp/ and calls the 6 v1 tool functions directly. Real payloads flow through last-panel-update; mock data remains as fallback if adwright_core import fails (proto version mismatch, missing adwright-mcp dir, etc). Verified in webui's hermes-agent venv (Python 3.11 + protobuf 6.x): - get_connections_status → real Plan B sandbox act_967504505966162 - list_cycles / list_segments / list_recipes → real (empty) data - refresh_cycles → real timestamp + cycle re-fetch Workspace-internal dependency on adwright-mcp/ documented in CONNECTION-MAP (regen). Karpathy 4 rules: smallest possible change to lift mock→real (single function add + handler tweak), no abstraction layer introduced, fallback preserves uptime if adwright-mcp evolves incompatibly. Co-Authored-By: Claude Opus 4.7 (1M context) --- CONNECTION-MAP.md | 6 ++-- routes/adwright.py | 83 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/CONNECTION-MAP.md b/CONNECTION-MAP.md index 426f192..09e2d80 100644 --- a/CONNECTION-MAP.md +++ b/CONNECTION-MAP.md @@ -22,9 +22,9 @@ | `plugin.py:41` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/adwright.js")` | | `plugin.py:46` | `api.inject_stylesheet` | `api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/bte.css")` | | `plugin.py:47` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/bte.js")` | -| `routes/adwright.py:43` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` | -| `routes/adwright.py:44` | `api.register_route` | `api.register_route(` | -| `routes/adwright.py:46` | `api.register_route` | `api.register_route(` | +| `routes/adwright.py:68` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` | +| `routes/adwright.py:69` | `api.register_route` | `api.register_route(` | +| `routes/adwright.py:71` | `api.register_route` | `api.register_route(` | | `routes/bte_proxy.py:40` | `api.logger` | `log = api.logger("svrnty.routes.bte_proxy")` | | `routes/bte_proxy.py:41` | `api.register_route` | `api.register_route("/api/bte/proxy", "GET", _handle_proxy)` | | `routes/bte_proxy.py:42` | `api.register_route` | `api.register_route("/api/bte/proxy", "POST", _handle_proxy)` | diff --git a/routes/adwright.py b/routes/adwright.py index 9ec5330..b0f8c56 100644 --- a/routes/adwright.py +++ b/routes/adwright.py @@ -3,24 +3,49 @@ Per ADWRIGHT-PANEL-PRD §5-§6: GET /api/adwright/last-panel-update — read channel for the frontend panel (polled while a /adwright cmd is - pending; reads from session DB). - v1 returns mocked data per tool name - while adwright-mcp is still wiring - its writes (PRD §5 [8] — TBD). + pending). v2: imports adwright_core + from the adwright-mcp subdir and + calls Adwright gRPC directly. Falls + back to mock if import fails. POST /api/adwright/provision-creds — governance exception write path. Writes Meta + Woo credentials direct to the credctl vault, bypassing chat and MCP. NO secret ever logged. Public API surface used: api.register_route, api.logger. -No forced internal dependencies — uses subprocess to call credctl directly. +adwright_core import is a workspace-internal dependency on adwright-mcp/ +(same hermes/ workspace; same Python venv at runtime). Documented in +CONNECTION-MAP.md. """ import json import os import subprocess +import sys import time import urllib.parse +# Lazy-import adwright_core from the adwright-mcp subdir. Only loaded when +# the panel first asks for real data. If the import fails (proto version +# mismatch, adwright-mcp not present), routes fall back to mock data. +_ADWRIGHT_MCP_PATH = "/home/svrnty/workspaces/hermes/adwright-mcp" +_adwright_core = None +_adwright_core_err: str | None = None + + +def _load_adwright_core(): + global _adwright_core, _adwright_core_err + if _adwright_core is not None or _adwright_core_err is not None: + return _adwright_core + try: + if _ADWRIGHT_MCP_PATH not in sys.path: + sys.path.insert(0, _ADWRIGHT_MCP_PATH) + import adwright_core # type: ignore + _adwright_core = adwright_core + return _adwright_core + except Exception as e: + _adwright_core_err = f"{type(e).__name__}: {e}" + return None + # Same credctl path as routes/vault_status.py (consistency). _DEFAULT_CREDCTL = "/home/svrnty/workspaces/cortex/L6-svrnty.core-credentials/credctl" @@ -109,6 +134,41 @@ def _mock_payload_for(tool: str) -> dict: return {} +# Real-data path — calls adwright_core directly. adwright_core tool functions +# return JSON strings; we parse + return the dict for the panel. +# Returns None when the tool isn't recognized or adwright_core unavailable. +def _real_payload_for(tool: str, qs: dict) -> dict | None: + core = _load_adwright_core() + if core is None: + return None + try: + if tool == "adwright_list_cycles": + limit = int((qs.get("limit", ["20"])[0] or "20")) + raw = core.list_cycles_tool(limit=limit) + elif tool == "adwright_refresh_cycles": + raw = core.refresh_cycles_tool() + # Also load fresh cycle list for the panel to re-render. + cycles_raw = core.list_cycles_tool(limit=20) + refresh_dict = json.loads(raw) + cycles_dict = json.loads(cycles_raw) + return {**refresh_dict, **cycles_dict} + elif tool == "adwright_get_cycle": + cycle_id = int((qs.get("cycle_id", ["0"])[0] or "0")) + raw = core.get_cycle_tool(cycle_id=cycle_id) + elif tool == "adwright_list_segments": + raw = core.list_segments_tool() + elif tool == "adwright_list_recipes": + limit = int((qs.get("limit", ["10"])[0] or "10")) + raw = core.list_recipes_tool(limit=limit) + elif tool == "adwright_get_connections_status": + raw = core.get_connections_status_tool() + else: + return None + return json.loads(raw) if isinstance(raw, str) else raw + except Exception: + return None + + def _handle_last_panel_update(handler, parsed): """GET /api/adwright/last-panel-update?session_id=&since=&tool= @@ -133,11 +193,16 @@ def _handle_last_panel_update(handler, parsed): if since_ts and (now_ms - since_ts) < 500: # Spec polls every 2s; if frontend just got an update <500ms ago, # return empty so we don't double-render. - return _send_json(handler, {"update": None, "mock": True}, 200) + return _send_json(handler, {"update": None}, 200) - payload = _mock_payload_for(tool) + # Real-data first; mock fallback if adwright_core unavailable. + payload = _real_payload_for(tool, qs) + is_mock = False + if payload is None: + payload = _mock_payload_for(tool) + is_mock = True if not payload: - return _send_json(handler, {"update": None, "mock": True}, 200) + return _send_json(handler, {"update": None, "mock": is_mock}, 200) return _send_json(handler, { "update": { @@ -145,7 +210,7 @@ def _handle_last_panel_update(handler, parsed): "tool": tool, "payload": payload, }, - "mock": True, + "mock": is_mock, }, 200)