Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,38 @@ The installer writes:
```text
$HERMES_HOME/desktop-plugins/visual-workbench/plugin.js
$HERMES_HOME/skills/midjourney-visual-workbench/SKILL.md
$HERMES_HOME/plugins/visual-workbench/plugin.yaml
$HERMES_HOME/plugins/visual-workbench/__init__.py
$HERMES_HOME/plugins/visual-workbench/dashboard/manifest.json
$HERMES_HOME/plugins/visual-workbench/dashboard/plugin_api.py
```

When `HERMES_HOME` is unset, it uses `~/.hermes`.

Hermes Desktop watches this directory and normally hot-loads the plugin. If it does not appear, open the command palette and run **Reload desktop plugins**.
Hermes Desktop watches the desktop-plugin directory and normally hot-loads the JavaScript plugin. The Python backend/dashboard files require a backend restart; the installer prints the matching `hermes plugins enable visual-workbench` reminder.

### Update

Run the install command again. Changed plugin and skill files are backed up separately before replacement. The install marker records both hashes.
Run the install command again. Changed managed files and the prior marker are backed up with one transaction stamp before replacement; unchanged files remain part of that same prior state.

### Rollback

Preview or restore the exact newest update transaction:

```bash
npx --yes github:HeiTuz/hermes-visual-workbench -- --rollback --dry-run
npx --yes github:HeiTuz/hermes-visual-workbench -- --rollback
```

Rollback restores only files changed by that transaction, keeps unchanged managed files, removes files that did not exist in the prior marker, and restores the prior marker bytes. It never combines per-file backups from different updates.

### Uninstall

```bash
npx --yes github:HeiTuz/hermes-visual-workbench -- --uninstall
```

The uninstaller refuses to delete either managed file when its hash changed. It pins deletion to the expected plugin and skill paths instead of trusting paths stored in the marker. Use `--force` only when you intentionally want to remove local modifications.
The uninstaller refuses to delete any of the six managed files when its hash changed. It pins deletion to the expected plugin, skill, backend, and dashboard paths instead of trusting paths stored in the marker. Use `--force` only when you intentionally want to remove local modifications.

### Custom Hermes home or test target

Expand All @@ -78,7 +93,7 @@ npx --yes github:HeiTuz/hermes-visual-workbench -- --hermes-home /path/to/.herme
npx --yes github:HeiTuz/hermes-visual-workbench -- --target /tmp/visual-workbench --skill-target /tmp/midjourney-skill
```

`--target` and `--skill-target` are a required pair so a test install cannot accidentally write the skill into the real Hermes home. Installation preflights regular files, rejects managed symlinks, and rolls back earlier writes if a later managed write fails.
`--target` and `--skill-target` are a required pair so a test install cannot accidentally write the skill into the real Hermes home. Installation preflights every managed destination, backup, temporary file, marker, and ancestor; rejects containment escapes and symlinks; and rolls back earlier writes and directory creation if a later operation fails.

## Hermes compatibility

Expand All @@ -100,14 +115,27 @@ Until the upstream capability PR is merged, use a Hermes Desktop build containin

The plugin owns workflow and presentation. Hermes core remains authoritative for privileged guest creation, navigation filtering, popup denial, permission handling, attachment handling, and PNG capture bounds.

The plugin does not ship a Python backend and does not copy credentials.
The package ships a bounded Python backend/dashboard bridge for local control and result receipts. It creates its own mode-`0600` local control token under the plugin directory and never copies external provider credentials.

It uses the existing `persist:hermes-browser` partition as-is and never reads, exports, deletes, or migrates its cookies. Midjourney submit, upscale, and variation remain agent approval gates: the plugin displays state and QC but never clicks those controls itself.

Midjourney automation is pinned to the Browser pane inside the Hermes Desktop window. The workflow scopes desktop actions to `app="Hermes"` and explicitly forbids external Chrome, Safari, Arc, Brave, Edge, and isolated `browser_*` sessions. If the internal pane is unavailable, it stops instead of falling back to another browser.

The Browser pane displays a visible **Automation target** affordance. With the privileged guest active it reads `Automation target · Hermes internal Browser pane · persist:hermes-browser`; in iframe fallback it reads `Automation target unavailable`, and the packaged workflow hard-stops as `internal_pane_unavailable` instead of retargeting any external browser or isolated `browser_*` session. Agents must re-verify this affordance from a fresh `app="Hermes"` capture immediately before every pointer, focus, or type action.

## Higgsfield read-only CLI inspection

Existing Higgsfield jobs can still be inspected through the authenticated `higgsfield` CLI without an MCP tool card. `scripts/higgsfield-control.mjs` maps only observation subcommands (`account status`, `generate list`, `soul-id list`, `model list`, `generate get`) and has no argv passthrough, so paid or mutating subcommands are unreachable by construction.

```bash
node scripts/higgsfield-control.mjs evidence --url "<result_url>"
node scripts/higgsfield-control.mjs evidence --job-id "<job-id>"
```

CLI evidence is diagnostic only and cannot be attached to QC through `set-target`. Trusted Web provenance is created inside Hermes: run the typed Higgsfield `observe` action on a completed result, then consume its short-lived one-shot `observationReceipt` with the typed `link` action. The Electron observer verifies the Unlimited UI state and exact result provenance; the plugin strips signed URL data before durable storage.

The bridge never uses the `hf` alias, which commonly resolves to the HuggingFace CLI; it always invokes `higgsfield` (override with `HIGGSFIELD_BIN`). This read-only bridge never triggers generation.

## Non-billable fixture E2E

Create a complete local artifact job without a Midjourney submission:
Expand Down
117 changes: 117 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Visual Workbench local bearer-token provider for its bounded control API."""
from __future__ import annotations

import hmac
import os
import secrets
import stat
from pathlib import Path
from typing import Optional

from hermes_cli.dashboard_auth.base import (
DashboardAuthProvider,
LoginStart,
Session,
TokenPrincipal,
)

_PLUGIN_NAME = "visual-workbench"
_TOKEN_FILE = "control.token"
_TOKEN_ROUTES = (
"/api/plugins/visual-workbench/command",
"/api/plugins/visual-workbench/control/result",
)


def _plugin_dir() -> Path:
from hermes_constants import get_hermes_home

return Path(get_hermes_home()) / "plugins" / _PLUGIN_NAME


def _read_or_create_token() -> str:
directory = _plugin_dir()
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
token_path = directory / _TOKEN_FILE
try:
fd = os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
info = token_path.lstat()
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode):
raise RuntimeError(f"Visual Workbench control token must be a regular file: {token_path}")
if info.st_mode & 0o077:
raise RuntimeError(f"Visual Workbench control token permissions are too broad: {token_path}")
token = token_path.read_text(encoding="utf-8").strip()
if len(token) < 43:
raise RuntimeError("Visual Workbench control token is missing or too short")
return token

try:
token = secrets.token_urlsafe(48)
os.write(fd, f"{token}\n".encode("utf-8"))
os.fsync(fd)
finally:
os.close(fd)
return token


class VisualWorkbenchTokenProvider(DashboardAuthProvider):
name = "visual-workbench-local"
display_name = "Visual Workbench local control"
supports_token = True
supports_session = False

def __init__(self, token: str) -> None:
if len(token) < 43:
raise ValueError("Visual Workbench control token must contain at least 256 bits")
self._token = token

def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
if token and hmac.compare_digest(token.encode("utf-8"), self._token.encode("utf-8")):
return TokenPrincipal(
principal="visual-workbench-local-control",
provider=self.name,
scopes=("visual-workbench-control",),
)
return None

def start_login(self, *, redirect_uri: str) -> LoginStart:
raise NotImplementedError("Visual Workbench local control has no interactive login flow")

def complete_login(
self, *, code: str, state: str, code_verifier: str, redirect_uri: str
) -> Session:
raise NotImplementedError("Visual Workbench local control has no interactive login flow")

def verify_session(self, *, access_token: str) -> Optional[Session]:
return None

def refresh_session(self, *, refresh_token: str) -> Session:
raise NotImplementedError("Visual Workbench local control has no interactive login flow")

def revoke_session(self, *, refresh_token: str) -> None:
return None


def _host_auth_supported() -> bool:
try:
from hermes_cli import __version__
from hermes_cli import web_server
parts = tuple(int(part) for part in __version__.split(".")[:3])
except Exception:
return False
# Capability-gated: no hard upper bound. Support any host at or above the floor
# that still exposes a callable token-auth capability; a missing capability or a
# below-floor host fails closed so a breaking version bump is caught, not assumed.
return parts >= (0, 18, 2) and callable(getattr(web_server, "_ws_auth_ok", None))

def register(ctx) -> None:
if not _host_auth_supported():
return
token = _read_or_create_token()
ctx.register_dashboard_auth_provider(VisualWorkbenchTokenProvider(token))

from hermes_cli.dashboard_auth.token_auth import register_token_route

for path in _TOKEN_ROUTES:
register_token_route(path)
7 changes: 7 additions & 0 deletions backend/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
manifest_version: 1
name: visual-workbench
version: 0.7.0
kind: standalone
host_version: ">=0.18.2"
description: "Local Visual Workbench dashboard bridge for browser capture, QC, and bounded Midjourney control."
author: "Yuna"
Loading
Loading