diff --git a/hermes_cli/plugin_capabilities.py b/hermes_cli/plugin_capabilities.py index 143121b9eb..a18b64319e 100644 --- a/hermes_cli/plugin_capabilities.py +++ b/hermes_cli/plugin_capabilities.py @@ -191,9 +191,8 @@ def record_consent(plugin_id: str, granted: Iterable[str], declared: Iterable[st save_config(config) logger.info( - "capability_consent plugin=%s granted=%s declared_hash=%s", - plugin_id, ",".join(entry[GRANTED_KEY]) or "(none)", - entry[CONSENT_KEY]["hash"][:12], + "capability_consent plugin=%s granted=%s declared_hash=%s", plugin_id, + ",".join(entry[GRANTED_KEY]) or "(none)", entry[CONSENT_KEY]["hash"][:12], ) diff --git a/hermes_cli/plugin_dev.py b/hermes_cli/plugin_dev.py index 87d6cf60ba..4118fa8ff2 100644 --- a/hermes_cli/plugin_dev.py +++ b/hermes_cli/plugin_dev.py @@ -85,9 +85,7 @@ def _doctor_runtime(plugin_path: Path): if not loaded.enabled: raise _DoctorLoadError("Plugin registration did not enable the runtime record") yield SimpleNamespace( - manifest=manifest, - manager=manager, - registered_tools=tuple(sorted(loaded.tools_registered)), + manifest=manifest, manager=manager, registered_tools=tuple(sorted(loaded.tools_registered)), registered_hooks=tuple(loaded.hooks_registered), ) finally: @@ -358,9 +356,4 @@ def doctor_plugin(target: str | os.PathLike[str] | None = None) -> DoctorReport: return report -__all__ = [ - "DoctorFinding", - "DoctorReport", - "doctor_plugin", - "resolve_plugin_path", -] +__all__ = ["DoctorFinding", "DoctorReport", "doctor_plugin", "resolve_plugin_path"] diff --git a/hermes_cli/plugin_index.py b/hermes_cli/plugin_index.py index 4b910eea98..26af39ceab 100644 --- a/hermes_cli/plugin_index.py +++ b/hermes_cli/plugin_index.py @@ -201,11 +201,8 @@ def _score_entry(entry: PluginIndexEntry, term: str) -> float: return 100.0 ratio = difflib.SequenceMatcher(None, term, name).ratio() # typo tolerance on the name signals = ( - (term in name, 80.0), - (term in tags, 70.0), - (any(term in t for t in tags), 55.0), - (term in entry.description.lower(), 50.0), - (term in entry.author.lower(), 40.0), + (term in name, 80.0), (term in tags, 70.0), (any(term in t for t in tags), 55.0), + (term in entry.description.lower(), 50.0), (term in entry.author.lower(), 40.0), (ratio >= 0.6, ratio * 60.0), ) return max((points for hit, points in signals if hit), default=0.0) diff --git a/hermes_cli/plugin_packs.py b/hermes_cli/plugin_packs.py index c1d9e6e500..42b97b04fc 100644 --- a/hermes_cli/plugin_packs.py +++ b/hermes_cli/plugin_packs.py @@ -18,9 +18,7 @@ logger = logging.getLogger(__name__) _EXACT_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") # Secret-shaped keys are refused in config seeds and stripped from exports; packs declare secrets # via each plugin's ``requires_env``, which prompts at install time. -_SECRET_KEY_RE = re.compile( - r"(?i)(token|secret|passw(or)?d|api[_-]?key|private[_-]?key|credential|auth)" -) +_SECRET_KEY_RE = re.compile(r"(?i)(token|secret|passw(or)?d|api[_-]?key|private[_-]?key|credential|auth)") # plugins.entries. keys a pack may never set (consent state; allow_* gates are also refused). _RESERVED_ENTRY_KEYS = frozenset({"granted_capabilities", "capabilities_consent"}) _MAX_PACK_BYTES = 1 * 1024 * 1024 # a pack is a small manifest, not a payload @@ -160,13 +158,9 @@ def parse_pack(text: str, *, source: str = "") -> PluginPack: raise PackError(f"Pack {source} 'skills' must be a list of skill ids.") return PluginPack( - name=name.strip(), - description=str(meta.get("description") or ""), - author=str(meta.get("author") or ""), - version=str(meta.get("version") or ""), - plugins=entries, - config=config, - skills=[str(s).strip() for s in skills_raw if str(s).strip()], + name=name.strip(), description=str(meta.get("description") or ""), + author=str(meta.get("author") or ""), version=str(meta.get("version") or ""), plugins=entries, + config=config, skills=[str(s).strip() for s in skills_raw if str(s).strip()], ) @@ -197,9 +191,7 @@ def _parse_pack_entry(item: Any, label: str) -> PackPluginEntry: if subdir is not None and (not isinstance(subdir, str) or not subdir.strip("/")): raise PackError(f"Pack plugin entry {label} has an invalid 'subdir'.") return PackPluginEntry( - ref=ref.strip().lower(), - name=entry_name, - repo=entry_repo, + ref=ref.strip().lower(), name=entry_name, repo=entry_repo, subdir=subdir.strip("/") if isinstance(subdir, str) else None, ) @@ -439,9 +431,7 @@ def install_pack_plugins( # Export # --------------------------------------------------------------------------- -_GITHUB_HTTPS_RE = re.compile( - r"^https://github\.com/(?P[^/\s]+)/(?P[^/\s#]+?)(?:\.git)?$" -) +_GITHUB_HTTPS_RE = re.compile(r"^https://github\.com/(?P[^/\s]+)/(?P[^/\s#]+?)(?:\.git)?$") def _source_to_repo_subdir(source: str) -> tuple[Optional[str], Optional[str]]: @@ -486,9 +476,7 @@ def export_pack(*, enabled_only: bool = False, pack_name: str = "my-hermes-pack" metadata = _read_install_metadata() enabled = _get_enabled_set() - installed = sorted( - d.name for d in _plugins_dir().iterdir() if d.is_dir() and not d.name.startswith(".") - ) + installed = sorted(d.name for d in _plugins_dir().iterdir() if d.is_dir() and not d.name.startswith(".")) if enabled_only: installed = [n for n in installed if n in enabled] diff --git a/hermes_cli/plugins_cmd.py b/hermes_cli/plugins_cmd.py index 605383d1a8..dd6a31d0df 100644 --- a/hermes_cli/plugins_cmd.py +++ b/hermes_cli/plugins_cmd.py @@ -177,9 +177,7 @@ def _sanitize_plugin_name(name: str, plugins_dir: Path, *, allow_subdir: bool = if not name: raise ValueError("Plugin name must not be empty.") if name in {".", ".."}: - raise ValueError( - f"Invalid plugin name '{name}': must not reference the plugins directory itself." - ) + raise ValueError(f"Invalid plugin name '{name}': must not reference the plugins directory itself.") for bad in ("\\", "..") if allow_subdir else ("/", "\\", ".."): if bad in name: raise ValueError(f"Invalid plugin name '{name}': must not contain '{bad}'.") @@ -187,13 +185,9 @@ def _sanitize_plugin_name(name: str, plugins_dir: Path, *, allow_subdir: bool = target = (plugins_dir / name).resolve() plugins_resolved = plugins_dir.resolve() if target == plugins_resolved: - raise ValueError( - f"Invalid plugin name '{name}': resolves to the plugins directory itself." - ) + raise ValueError(f"Invalid plugin name '{name}': resolves to the plugins directory itself.") if plugins_resolved not in target.parents: - raise ValueError( - f"Invalid plugin name '{name}': resolves outside the plugins directory." - ) + raise ValueError(f"Invalid plugin name '{name}': resolves outside the plugins directory.") return target @@ -250,9 +244,7 @@ def _resolve_subdir_within(clone_root: Path, subdir: str) -> Path: if candidate != clone_root and clone_root not in candidate.parents: raise PluginOperationError(f"Plugin subdirectory '{subdir}' escapes the repository.") if not candidate.exists(): - raise PluginOperationError( - f"Plugin subdirectory '{subdir}' does not exist in the repository.", - ) + raise PluginOperationError(f"Plugin subdirectory '{subdir}' does not exist in the repository.") if not candidate.is_dir(): raise PluginOperationError(f"Plugin subdirectory '{subdir}' is not a directory.") return candidate @@ -471,9 +463,7 @@ def _write_install_metadata(metadata: dict[str, dict[str, object]]) -> None: """Atomically replace the profile-local plugin install metadata sidecar.""" path = _install_metadata_path() atomic_write_text( - path, - json.dumps(metadata, indent=2, sort_keys=True) + "\n", - tmp_prefix=f"{path.name}.tmp-", + path, json.dumps(metadata, indent=2, sort_keys=True) + "\n", tmp_prefix=f"{path.name}.tmp-", ) @@ -801,9 +791,7 @@ def cmd_install( console.print() console.print("[yellow]⚠ Security scan flagged this plugin:[/yellow]") console.print(format_scan_report(scan_result)) - return _is_tty() and _ask_yes( - " Install anyway? Only continue if you trust the source. [y/N]: " - ) + return _is_tty() and _ask_yes(" Install anyway? Only continue if you trust the source. [y/N]: ") try: target, installed_manifest, installed_name = _install_plugin_core( @@ -1589,8 +1577,7 @@ def _configure_category_spec(spec) -> bool: """Launch the radio picker for one ``_PROVIDER_CATEGORY_SPECS`` row. Returns True if changed.""" title, default_label, default_name, current, discover, save = spec return _configure_provider_category( - f"{title} (select one)", f"{default_label} (default)", default_name, - current(), discover(), save, + f"{title} (select one)", f"{default_label} (default)", default_name, current(), discover(), save, ) @@ -1892,11 +1879,8 @@ def dashboard_install_plugin(identifier: str, *, force: bool, enable: bool) -> d for f in (exc.scan_result.findings if exc.scan_result is not None else ()) ] return { - "ok": False, - "error": str(exc), - "scan_blocked": True, - "scan_verdict": getattr(exc.scan_result, "verdict", "dangerous"), - "scan_findings": findings, + "ok": False, "error": str(exc), "scan_blocked": True, + "scan_verdict": getattr(exc.scan_result, "verdict", "dangerous"), "scan_findings": findings, } except PluginOperationError as exc: return {"ok": False, "error": str(exc)} @@ -1905,12 +1889,9 @@ def dashboard_install_plugin(identifier: str, *, force: bool, enable: bool) -> d _set_plugin_enabled(installed_name, enable=True) ap = target / "after-install.md" return { - "ok": True, - "plugin_name": installed_name, - "warnings": warnings, + "ok": True, "plugin_name": installed_name, "warnings": warnings, "missing_env": [s["name"] for s in _missing_env_specs(installed_manifest)], - "after_install_path": str(ap) if ap.exists() else None, - "enabled": enable, + "after_install_path": str(ap) if ap.exists() else None, "enabled": enable, } @@ -2055,13 +2036,8 @@ def _run_plugin_git( ) -> subprocess.CompletedProcess: """Run one git command inside a plugin checkout (non-interactive).""" return subprocess.run( - [git_exe, *args], - capture_output=True, - text=True, encoding='utf-8', errors='replace', - timeout=timeout, - cwd=str(target), - stdin=subprocess.DEVNULL, - env=noninteractive_git_env(), + [git_exe, *args], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout, + cwd=str(target), stdin=subprocess.DEVNULL, env=noninteractive_git_env(), ) @@ -2094,9 +2070,8 @@ def _git_pull_plugin_dir(target: Path) -> tuple[bool, str]: if status.returncode == 0 and status.stdout.strip(): pre_stash = _stash_ref(git_exe, target) push = _run_plugin_git( - git_exe, target, - "stash", "push", "--include-untracked", - "-m", "hermes-plugin-update-autostash", + git_exe, target, "stash", "push", "--include-untracked", "-m", + "hermes-plugin-update-autostash", ) post_stash = _stash_ref(git_exe, target) stash_created = bool(post_stash) and post_stash != pre_stash diff --git a/hermes_cli/plugins_dispatch.py b/hermes_cli/plugins_dispatch.py index 32f19c70b1..4028d3e2eb 100644 --- a/hermes_cli/plugins_dispatch.py +++ b/hermes_cli/plugins_dispatch.py @@ -22,7 +22,6 @@ from hermes_cli.middleware import OBSERVER_SCHEMA_VERSION logger = logging.getLogger("hermes_cli.plugins") - # Allowlist of agent-turn hot-path hooks bounded by plugins.hook_callback_timeout (fail-open: # abandon without join — joining reintroduced a shutdown hang). Unlisted hooks run synchronously. # Intentionally unbounded: on_session_finalize/reset (last-chance flush — abandon can lose state); @@ -34,7 +33,6 @@ _HOOK_TIMEOUT_BOUNDED_HOOKS: Set[str] = { "pre_verify", "on_session_start", "on_session_end", } - # Policy hooks: timeout / still-running must fail closed (block the tool). _HOOK_TIMEOUT_FAIL_CLOSED_HOOKS: Set[str] = {"pre_tool_call"} # Documented parent-thread serialization contract — never run on a timeout worker (hooks.md). @@ -192,8 +190,7 @@ class PluginDispatchMixin: results.append(ret) except Exception as exc: logger.warning( - "Hook '%s' callback %s raised: %s", hook_name, - getattr(cb, "__name__", repr(cb)), exc, + "Hook '%s' callback %s raised: %s", hook_name, getattr(cb, "__name__", repr(cb)), exc, ) return results @@ -235,9 +232,7 @@ class PluginDispatchMixin: self._hook_running_callbacks.pop(callback_key, None) done.set() - thread = threading.Thread( - target=_runner, name=f"hermes-hook-{callback_name}"[:40], daemon=True, - ) + thread = threading.Thread(target=_runner, name=f"hermes-hook-{callback_name}"[:40], daemon=True) thread.start() if not done.wait(timeout=timeout): # do not join — that would reintroduce the hang with self._hook_timeout_lock: @@ -245,8 +240,7 @@ class PluginDispatchMixin: time.monotonic() + self._hook_timeout_suppression_seconds ) logger.warning( - "Hook '%s' callback %s timed out after %gs — skipping", hook_name, callback_name, - timeout, + "Hook '%s' callback %s timed out after %gs — skipping", hook_name, callback_name, timeout, ) return _HOOK_SKIPPED if "exc" in failure: @@ -464,7 +458,6 @@ class PluginDispatchMixin: results.append(ret) except Exception as exc: logger.warning( - "Middleware '%s' callback %s raised: %s", kind, - getattr(cb, "__name__", repr(cb)), exc, + "Middleware '%s' callback %s raised: %s", kind, getattr(cb, "__name__", repr(cb)), exc, ) return results