feat(adwright route): wire real Adwright data via adwright_core import
Some checks failed
plugin-tests / test (push) Failing after 6s

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) <noreply@anthropic.com>
This commit is contained in:
Svrnty 2026-05-24 12:16:46 -04:00
parent 0b19fdd7d0
commit 4dac80b215
2 changed files with 77 additions and 12 deletions

View File

@ -22,9 +22,9 @@
| `plugin.py:41` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/adwright.js")` | | `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: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")` | | `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:68` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` |
| `routes/adwright.py:44` | `api.register_route` | `api.register_route(` | | `routes/adwright.py:69` | `api.register_route` | `api.register_route(` |
| `routes/adwright.py:46` | `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: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: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)` | | `routes/bte_proxy.py:42` | `api.register_route` | `api.register_route("/api/bte/proxy", "POST", _handle_proxy)` |

View File

@ -3,24 +3,49 @@
Per ADWRIGHT-PANEL-PRD §5-§6: Per ADWRIGHT-PANEL-PRD §5-§6:
GET /api/adwright/last-panel-update read channel for the frontend panel GET /api/adwright/last-panel-update read channel for the frontend panel
(polled while a /adwright cmd is (polled while a /adwright cmd is
pending; reads from session DB). pending). v2: imports adwright_core
v1 returns mocked data per tool name from the adwright-mcp subdir and
while adwright-mcp is still wiring calls Adwright gRPC directly. Falls
its writes (PRD §5 [8] TBD). back to mock if import fails.
POST /api/adwright/provision-creds governance exception write path. POST /api/adwright/provision-creds governance exception write path.
Writes Meta + Woo credentials direct Writes Meta + Woo credentials direct
to the credctl vault, bypassing chat to the credctl vault, bypassing chat
and MCP. NO secret ever logged. and MCP. NO secret ever logged.
Public API surface used: api.register_route, api.logger. 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 json
import os import os
import subprocess import subprocess
import sys
import time import time
import urllib.parse 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). # Same credctl path as routes/vault_status.py (consistency).
_DEFAULT_CREDCTL = "/home/svrnty/workspaces/cortex/L6-svrnty.core-credentials/credctl" _DEFAULT_CREDCTL = "/home/svrnty/workspaces/cortex/L6-svrnty.core-credentials/credctl"
@ -109,6 +134,41 @@ def _mock_payload_for(tool: str) -> dict:
return {} 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): def _handle_last_panel_update(handler, parsed):
"""GET /api/adwright/last-panel-update?session_id=&since=&tool= """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: if since_ts and (now_ms - since_ts) < 500:
# Spec polls every 2s; if frontend just got an update <500ms ago, # Spec polls every 2s; if frontend just got an update <500ms ago,
# return empty so we don't double-render. # 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: 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, { return _send_json(handler, {
"update": { "update": {
@ -145,7 +210,7 @@ def _handle_last_panel_update(handler, parsed):
"tool": tool, "tool": tool,
"payload": payload, "payload": payload,
}, },
"mock": True, "mock": is_mock,
}, 200) }, 200)