svrnty-hermes-webui-plugin/plugin.py
Svrnty 0b19fdd7d0
Some checks failed
plugin-tests / test (push) Failing after 5s
feat(plugin): Adwright + BTE Command Center panels (v0.4.0)
Two new tool panels surfaced inside hermes-webui via inject_script/
inject_stylesheet. Both vanilla JS + CSS, no frameworks, WebUI CSS-vars
only (no hardcoded colors), light/dark inherits free.

## Adwright panel (static/adwright.{js,css} + routes/adwright.py)

5 tabs: Overview · Cycles · Audience · Targeting · Connections.
Layout: 60/40 panel/chat split via CSS :has() selector.
Always-visible, soft-disabled when active profile isn't `cmo*`.

Action wiring (READ path — agent-mediated per governance):
1. Panel button → fires custom event
2. Handler synthesizes /adwright <cmd> chat message
3. Posts via existing btnSend pathway → message visible in chat
4. CMO sees + calls mcp_adwright_<tool>
5. Panel polls /api/adwright/last-panel-update for structured payload
6. Mock payload returned v1; real session-DB reader plugs in when
   adwright-mcp gains writer

Connections WRITE path (governance exception, NO secrets in chat):
- POST /api/adwright/provision-creds with form fields
- Plugin invokes credctl set <key> via stdin (value never on argv)
- Allowlist enforced (defense-in-depth on key names)
- Auth-gated by WebUI session cookie

Skin: .svrnty-aw-* class prefix, window.SvrntyAdwright JS namespace,
guard against double-load, scoped MutationObserver.

## BTE Command Center panel (static/bte.{js,css} + routes/bte_proxy.py)

Content-mode pills (Polished/UGC/Photorealistic/Artistic) × media toggle
(Image/Video — Video disabled v1 pending Phase 4e) × recipe family picker
(Hero Shot/Lifestyle Shot/Photoshoot/Recipe Sheet/Montage Catalog) per
canonical PLANB-RECIPE-TAXONOMY. SKU picker, variant stepper 1-12,
single/batch toggle, [Generate] button.

Asset grid with streaming thumbnails, asset detail (full-res + rate +
comment + "Use in Adwright cycle" deep link). Embedded CMO chat right rail
for re-orienting generations ("make next batch warmer / less white space").

BTE proxy route (/api/bte/proxy) with whitelisted paths
(requestPhotoshoot, assetGrid, recipeStats, assets/{id}/thumb, etc.)
prevents browser-side CORS to BTE :6001.

Skin: .svrnty-bte-* class prefix, window.SvrntyBTE JS namespace.

## Wiring

manifest_version: 0.2.0 → 0.4.0
assets registered:
- /plugins/svrnty/adwright.{js,css} + static/adwright/
- /plugins/svrnty/bte.{js,css} + static/bte/
routes registered:
- GET /api/adwright/last-panel-update (panel update channel)
- POST /api/adwright/provision-creds (governance-exception write)
- GET/POST /api/bte/proxy (BTE REST proxy with allowlist)

Karpathy 4 rules: agents reported every deviation with rationale (Python
venv interp for hermes mcp add, missing aggregate connections-status RPC
composed from two verifies, mock panel-update v1 with locked frontend
protocol so real session-DB reader is a drop-in swap), verified asset
serving + plugin route registration before claiming complete, surfaced
open questions instead of silently choosing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:12:27 -04:00

78 lines
3.6 KiB
Python

"""svrnty-hermes-webui-plugin — entry point.
Called by hermes-webui's plugin loader at startup (after the env var
HERMES_WEBUI_PYTHON_PLUGIN points the loader at this module).
The loader passes a single `api` argument exposing 6 methods (see CLAUDE.md +
protocol PRD §5.1). This module's job is to wire every route + static dir +
asset injection that defines the Svrnty surface on hermes-webui.
Keep this file thin. Route logic lives in `routes/<feature>.py`. Static lives
in `static/`. The map of every upstream dependency is in CONNECTION-MAP.md
(AST-generated by scripts/ast-connection-map.py).
"""
import os
from pathlib import Path
# Static + asset URL prefix (per protocol §12, decision Q5: /plugins/svrnty/<asset>)
STATIC_PREFIX = "svrnty"
STATIC_DIR = Path(__file__).resolve().parent / "static"
def register(api):
"""Wire every Svrnty modification to hermes-webui.
`api` is the loader-provided extension surface (6 methods). Treat it as the
ONLY public contract. Touching anything else in hermes-webui requires a
`CONNECTION-MAP.md` forced-internal entry with justification.
"""
log = api.logger("svrnty.plugin")
log.info("svrnty-hermes-webui-plugin: registering")
# Brand skin: serve static dir + inject CSS/JS into every page load.
if STATIC_DIR.exists():
api.register_static(STATIC_PREFIX, str(STATIC_DIR))
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/app.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/app.js")
# Adwright tool panel (ADWRIGHT-PANEL-PRD §7) — additional assets
# served from the same static dir, injected after the brand skin so
# adwright.css overrides any conflicting brand defaults.
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/adwright.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/adwright.js")
# BTE Command Center panel (COMMAND-CENTER-PRD §3 + PLANB-RECIPE-TAXONOMY).
# Independent IIFE under window.SvrntyBTE namespace; ordering doesn't
# matter — both panels coexist via distinct .svrnty-bte-* / adwright
# selectors and namespaces.
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/bte.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/bte.js")
log.info("static + assets wired at /plugins/%s/", STATIC_PREFIX)
# Routes — each feature lives in its own module under routes/.
# Phase 2 will populate these. Import-and-register pattern; failures are
# logged but don't take down the rest of the plugin.
for route_module in _phase2_routes():
try:
mod = __import__(f"routes.{route_module}", fromlist=["register"])
mod.register(api)
log.info("route module loaded: %s", route_module)
except ImportError as e:
log.warning("route module %s not yet implemented (Phase 2): %s", route_module, e)
except Exception as e:
log.error("route module %s failed to register: %s", route_module, e)
log.info("svrnty-hermes-webui-plugin: registration complete")
def _phase2_routes():
"""Routes to attempt loading. Returns module names under routes/.
Phase 2 migrates the existing fork commits into these modules. Until then,
ImportError is logged + swallowed so the plugin loads cleanly.
"""
return [
"transcribe", # P2.A — STT + voice-message audio processor ✓
"vault_status", # P2.B — vault connections status ✓
"adwright", # P2.C — Adwright tool panel routes (PRD §5+§6) ✓
"bte_proxy", # P2.D — BTE Command Center same-origin proxy (PRD §3) ✓
]