From 68aad9d6e5b529e441b3160dd18ffd4d7196204c Mon Sep 17 00:00:00 2001 From: TheStreamCode Date: Tue, 22 Sep 2026 20:08:28 +0200 Subject: [PATCH 1/2] chore: fleet hygiene (community files, CI permissions, dependabot) Add SECURITY.md (private advisories + info@mikesoft.it fallback + never-log-keys rule, adapted from hermes-chutes-provider), CONTRIBUTING.md, CODE_OF_CONDUCT.md (with private-contact line), and structured issue forms + PR template (adapted from hermes-chutes-provider). Add permissions: contents: read to ci.yml and grouped-weekly dependabot.yml (github-actions; the plugin is stdlib-only by design, so no pip entry). Closes the review P0 and P1 file items for hermes-muse-code. --- .github/ISSUE_TEMPLATE/bug.yml | 73 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/compatibility.yml | 53 ++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 13 ++++ .github/dependabot.yml | 18 ++++++ .github/workflows/ci.yml | 3 + CODE_OF_CONDUCT.md | 13 ++++ CONTRIBUTING.md | 23 +++++++ SECURITY.md | 24 ++++++++ 10 files changed, 247 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/compatibility.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..36616ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,73 @@ +name: Bug report +description: Report reproducible behavior in the Hermes Muse Code provider +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve this independent, unofficial integration. + Remove subscription keys, credentials, private prompts, and account data before submitting. + - type: textarea + id: summary + attributes: + label: Problem summary + description: What happened, and what did you expect instead? + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: Provide the smallest sequence that reproduces the problem. + placeholder: | + 1. Configure... + 2. Run... + 3. Observe... + validations: + required: true + - type: dropdown + id: installation + attributes: + label: Installation route + options: + - Manual directory install from a release tag + - Development checkout from main + - Entry-point or native install under development + - Other + validations: + required: true + - type: input + id: plugin-version + attributes: + label: Plugin version or commit + placeholder: v0.2.0 or a commit SHA + validations: + required: true + - type: input + id: hermes-version + attributes: + label: Hermes Agent version or commit + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: Include Python version and operating system. + placeholder: Python 3.12 on Windows 11 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Sanitized logs + description: Include only relevant output after removing sensitive data. + render: shell + - type: checkboxes + id: safety + attributes: + label: Sensitive-data check + options: + - label: I removed subscription keys, credentials, private prompts, and account data. + required: true diff --git a/.github/ISSUE_TEMPLATE/compatibility.yml b/.github/ISSUE_TEMPLATE/compatibility.yml new file mode 100644 index 0000000..545617c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility.yml @@ -0,0 +1,53 @@ +name: Compatibility report +description: Report a compatibility problem with a Hermes or Muse change +title: "[Compatibility]: " +labels: ["compatibility"] +body: + - type: markdown + attributes: + value: | + Use this form when the provider stops matching a released Hermes version or a documented Muse subscription behavior. + Remove subscription keys, credentials, private prompts, and account data before submitting. + - type: dropdown + id: affected-system + attributes: + label: Affected system + options: + - Hermes Agent + - Muse subscription login + - Both or unclear + validations: + required: true + - type: input + id: plugin-version + attributes: + label: Plugin version or commit + placeholder: v0.2.0 or a commit SHA + validations: + required: true + - type: input + id: upstream-version + attributes: + label: Hermes or Muse version reference + description: Provide a release, commit, dated behavior change, or authoritative link. + validations: + required: true + - type: textarea + id: observed-behavior + attributes: + label: Observed compatibility problem + description: Explain the regression and provide minimal reproduction steps. + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Sanitized evidence + description: Add relevant logs or links after removing sensitive data. + - type: checkboxes + id: safety + attributes: + label: Sensitive-data check + options: + - label: I removed subscription keys, credentials, private prompts, and account data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3c5fca1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://github.com/TheStreamCode/hermes-muse-code/security/advisories/new + about: Send sensitive findings privately. Never include credentials in a public issue. + - name: Hermes Agent support + url: https://github.com/NousResearch/hermes-agent/issues + about: Report issues that also occur without this provider to the Hermes Agent project. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..460e629 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an improvement for the Muse Code provider plugin +title: "[FEAT] " +labels: enhancement +assignees: '' +--- + +## Problem +What problem does this solve? + +## Proposed Solution +How you'd like it to work. + +## Alternatives Considered +Other approaches you've thought about. + +## Additional Context +Any other relevant information. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3b294aa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## Summary + +- + +## Validation + +- [ ] `python -m pytest -q` +- [ ] Upstream `plugin-validate` outcome noted (CI runs it non-blocking) + +## Compatibility and Security + +- [ ] No credentials, secrets, or private data are included +- [ ] Hermes Agent and Muse subscription compatibility claims were verified diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..aa240bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore(actions)" + include: "scope" + groups: + actions-minor-patch: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe7fb44..22ec988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: pull_request: +permissions: + contents: read + jobs: unit: runs-on: ubuntu-latest diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d78f166 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Code of Conduct + +Contributors are expected to keep discussion professional, constructive, and +focused on improving the project. + +Unacceptable behavior includes harassment, personal attacks, discriminatory +language, and knowingly misleading security, privacy, or compatibility claims. + +Maintainers may moderate issues, pull requests, and discussions to preserve a +safe and useful project environment. + +To report a conduct concern privately, email `info@mikesoft.it` with the +subject `Hermes Muse Code Code of Conduct Report`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..457d0e1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing to hermes-muse-code + +Thanks for your interest in improving this project! Bug reports, fixes, docs, +and tests are all welcome. This plugin stays stdlib-only by design — do not +add third-party runtime dependencies. + +## Validation + +Run the unit suite before opening a pull request: + +```bash +python -m pytest -q +``` + +CI also runs the upstream `plugin-validate` step in non-blocking mode (the +upstream pip guard rejects it; the failure reason is documented in `ci.yml`). + +## Pull requests + +- Keep changes focused; one concern per PR. +- Add or update tests for behavior changes. +- Never commit credentials, subscription keys, or account data. +- Never log the cached subscription key in code or fixtures. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5da03a2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + +## Reporting a Vulnerability + +Use GitHub's +[private vulnerability reporting](https://github.com/TheStreamCode/hermes-muse-code/security/advisories/new) +for sensitive findings. If that route is unavailable, email +`info@mikesoft.it` with the subject `Hermes Muse Code Security Report`. +Do not disclose API keys, credentials, private prompts, or account data in a +public issue. + +For non-sensitive security hardening, open an issue with the affected component, +observed behavior, impact, and a minimal reproduction where practical. + +## Supported Version + +Security fixes target the latest published release and the current `main` branch. + +## Credential Handling + +This project must never log, persist, or commit Muse subscription credentials. +The subscription key lives owner-only in the local credential cache and in +process memory; it is never written to logs. Local credentials belong in the +active Hermes profile's ignored environment configuration. From 5cf649bae70625ba71a58126504809a88e11fbf3 Mon Sep 17 00:00:00 2001 From: TheStreamCode Date: Tue, 22 Sep 2026 20:38:39 +0200 Subject: [PATCH 2/2] ci: replace broken upstream plugin-validate with local validator The upstream NousResearch/hermes-agent plugin-validate action installs hermes-agent via pip from git, but hermes-agent's own setup.py guard rejects wheel/sdist builds outside Nix (pip/PyPI unsupported since d84e11a). The action has a single revision in history, so no last-good version exists to pin. Replace the validate job with a checked-in stdlib-only script (.github/scripts/validate-plugin.py) performing the same admission checks (manifest schema, stdlib-only imports, capability probe) with no hermes-agent install and no upstream dependency, so the job gates green again. Update CONTRIBUTING.md and the PR template to reference the local validator. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/scripts/validate-plugin.py | 349 +++++++++++++++++++++++++++++ .github/workflows/ci.yml | 21 +- CONTRIBUTING.md | 6 +- 4 files changed, 367 insertions(+), 11 deletions(-) create mode 100644 .github/scripts/validate-plugin.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3b294aa..156ecdb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,7 +5,7 @@ ## Validation - [ ] `python -m pytest -q` -- [ ] Upstream `plugin-validate` outcome noted (CI runs it non-blocking) +- [ ] `python .github/scripts/validate-plugin.py --path .` (local plugin validator, runs in CI) ## Compatibility and Security diff --git a/.github/scripts/validate-plugin.py b/.github/scripts/validate-plugin.py new file mode 100644 index 0000000..9ac03f4 --- /dev/null +++ b/.github/scripts/validate-plugin.py @@ -0,0 +1,349 @@ +"""Local equivalent of the upstream `plugin-validate` action. + +Why this exists (no upstream dependency): the official +`NousResearch/hermes-agent/.github/actions/plugin-validate` action +pip-installs `hermes-agent` from git (`pip install +"git+https://github.com/NousResearch/hermes-agent@"`), but +hermes-agent's own `setup.py` build guard rejects wheel/sdist builds +outside a sealed Nix derivation ("Building wheels or sdists for +hermes-agent is not supported ... distributed via the shell installer, +Docker image, or Nix"). The action has a single revision in history, so +there is no last-good action version to pin — every ref fails the same +way. This script therefore performs the same admission checks locally, +stdlib-only, without installing hermes-agent: + + 1. plugin.yaml manifest schema (required fields, requires_hermes spec, + config_schema / requires_env shapes), + 2. stdlib-only imports (hermes-provided `agent` / `providers` exempt), + 3. capability probe: import registers a provider and `register(ctx)` + runs probe-safe against a recording stub context. + +Usage: python .github/scripts/validate-plugin.py [--path .] +""" + +from __future__ import annotations + +import argparse +import ast +import importlib.util +import os +import re +import sys +import tempfile +from pathlib import Path + +try: + import yaml +except ImportError: # pragma: no cover - CI installs pyyaml explicitly + yaml = None + +_VERSION_CLAUSE_RE = re.compile( + r"^(>=|<=|==|!=|>|<|~=)?\s*\d+(\.\d+)*(\.\*)?\s*$" +) +_UPPER_SNAKE_RE = re.compile(r"^[A-Z][A-Z0-9_]*$") +_CONFIG_TYPES = { + "str", "string", "int", "integer", "float", "number", + "bool", "boolean", "list", "array", "dict", "mapping", "map", +} +# Top-level modules provided by the Hermes runtime at load time (not pip +# dependencies), plus everything in the stdlib. +_RUNTIME_PROVIDED = {"agent", "providers"} + +_failures: list[str] = [] +_warnings: list[str] = [] + + +def check(name: str, ok: bool, detail: str = "") -> None: + status = "PASS" if ok else "FAIL" + print(f"[{status}] {name}" + (f": {detail}" if detail else "")) + if not ok: + _failures.append(f"{name}: {detail}" if detail else name) + + +def warn(message: str) -> None: + print(f"[WARN] {message}") + _warnings.append(message) + + +def _requires_hermes_spec_valid(spec: str) -> bool: + return all( + _VERSION_CLAUSE_RE.match(clause.strip()) + for clause in spec.split(",") + if clause.strip() + ) + + +def _check_manifest(manifest: dict) -> None: + missing = [f for f in ("name", "version", "description") if not manifest.get(f)] + check( + "manifest fields", + not missing, + "name, version, description present" + if not missing + else f"plugin.yaml missing required field(s): {', '.join(missing)}", + ) + spec = str(manifest.get("requires_hermes") or "").strip() + if not spec: + check("requires_hermes", True, "not declared") + else: + check( + "requires_hermes", + _requires_hermes_spec_valid(spec), + f"spec {spec!r} parses" + if _requires_hermes_spec_valid(spec) + else f"spec {spec!r} does not parse " + '(expected e.g. ">=0.21.3" or ">=0.21, <1.0")', + ) + raw = manifest.get("config_schema") + if raw in (None, [], {}): + check("config schema", True, "not declared") + elif not isinstance(raw, dict): + check("config schema", False, "config_schema: must be a mapping") + else: + problems = [] + for skey, entry in raw.items(): + if not isinstance(entry, dict): + problems.append(f"config_schema.{skey}: must be a mapping") + continue + typ = entry.get("type") + if typ is not None and str(typ).lower() not in _CONFIG_TYPES: + problems.append(f"config_schema.{skey}: bad type {typ!r}") + if "required" in entry and not isinstance(entry["required"], bool): + problems.append(f"config_schema.{skey}: required must be bool") + check( + "config schema", + not problems, + "shape valid" if not problems else "; ".join(problems), + ) + raw_env = manifest.get("requires_env") or [] + if not isinstance(raw_env, list): + check("requires_env", False, "requires_env: must be a list") + else: + problems = [] + for i, entry in enumerate(raw_env): + name = entry if isinstance(entry, str) else str((entry or {}).get("name") or "") + if not _UPPER_SNAKE_RE.match(name): + problems.append(f"requires_env[{i}]: {name!r} not UPPER_SNAKE_CASE") + check( + "requires_env", + not problems, + "all entries UPPER_SNAKE" if not problems else "; ".join(problems), + ) + + +def _check_stdlib_only(plugin_dir: Path) -> None: + stdlib = set(sys.stdlib_module_names) + problems = [] + for path in sorted(plugin_dir.glob("*.py")): + if path.name == "validate-plugin.py": + continue + try: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + except SyntaxError as exc: + problems.append(f"{path.name}: syntax error: {exc}") + continue + for node in ast.walk(tree): + if isinstance(node, ast.Import): + mods = [a.name.split(".")[0] for a in node.names] + elif isinstance(node, ast.ImportFrom): + if node.level: + continue # relative import within the plugin + mods = [node.module.split(".")[0]] if node.module else [] + else: + continue + for mod in mods: + if mod not in stdlib and mod not in _RUNTIME_PROVIDED: + problems.append(f"{path.name}: third-party import {mod!r}") + check( + "stdlib-only", + not problems, + "no third-party runtime imports" + if not problems + else "; ".join(sorted(set(problems))), + ) + + +class _RecordingContext: + plugin_config = {} + profile_name = "default" + plugin_id = "local_validate_probe" + + def __init__(self) -> None: + self.tools: list[str] = [] + self.hooks: list[str] = [] + self.middleware: list[str] = [] + + def register_tool(self, name, *args, **kwargs) -> None: + self.tools.append(str(name)) + + def register_hook(self, hook_name, callback) -> None: + self.hooks.append(str(hook_name)) + + def register_middleware(self, kind, callback) -> None: + self.middleware.append(str(kind)) + + def register_command(self, name, *args, **kwargs) -> None: + self.tools.append(str(name)) + + register_cli_command = register_command + + def get_config(self, key, default=None): + return default + + +def _check_capability_probe(plugin_dir: Path, manifest: dict) -> None: + init = plugin_dir / "__init__.py" + if not init.is_file(): + warn("no __init__.py — capability probe skipped (manifest-only plugin)") + check("capability probe", True, "skipped (no __init__.py)") + return + import types + + scratch = tempfile.mkdtemp(prefix="hermes-local-validate-") + os.environ["MUSE_CODE_SUB_CREDENTIALS"] = os.path.join(scratch, "absent.json") + os.environ.pop("MUSE_CODE_SUB_TOKEN", None) + + recorded_providers: list[str] = [] + + providers_mod = types.ModuleType("providers") + + def _record_provider(profile) -> None: + recorded_providers.append(str(getattr(profile, "name", profile))) + + providers_mod.register_provider = _record_provider # type: ignore[attr-defined] + + base_mod = types.ModuleType("providers.base") + + class ProviderProfile: # minimal stub of the runtime base + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def fetch_models(self, **kwargs): + return None + + base_mod.ProviderProfile = ProviderProfile # type: ignore[attr-defined] + + agent_mod = types.ModuleType("agent") + reasoning_mod = types.ModuleType("agent.reasoning_effort") + reasoning_mod.META_AI_EFFORTS = ( # type: ignore[attr-defined] + "minimal", "low", "medium", "high", "xhigh", "max", + ) + reasoning_mod.clamp_effort = lambda effort, efforts: ( # type: ignore[attr-defined] + effort if effort in efforts else "medium" + ) + + saved = {k: sys.modules.get(k) for k in ("providers", "providers.base", "agent", "agent.reasoning_effort")} + sys.modules["providers"] = providers_mod + sys.modules["providers.base"] = base_mod + sys.modules["agent"] = agent_mod + sys.modules["agent.reasoning_effort"] = reasoning_mod + try: + spec = importlib.util.spec_from_file_location( + "local_validate_probe_plugin", str(init), + submodule_search_locations=[str(plugin_dir)], + ) + module = importlib.util.module_from_spec(spec) + module.__path__ = [str(plugin_dir)] + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + except Exception as exc: # noqa: BLE001 - report, don't crash + check("capability probe", False, f"import failed: {exc}") + return + if manifest.get("kind") == "model-provider": + check( + "capability probe", + bool(recorded_providers), + "import registered provider(s): " + ", ".join(recorded_providers) + if recorded_providers + else "model-provider plugin registered no ProviderProfile at import", + ) + else: + check("capability probe", True, "import ok") + register = getattr(module, "register", None) + if register is None: + check("capability probe", False, "no register() function") + return + try: + ctx = _RecordingContext() + register(ctx) + except Exception as exc: # noqa: BLE001 - report, don't crash + check("capability probe", False, f"register() raised: {exc}") + return + for kind, attr, manifest_key in ( + ("tools", "tools", "provides_tools"), + ("hooks", "hooks", "provides_hooks"), + ("middleware", "middleware", "provides_middleware"), + ): + declared = set(manifest.get(manifest_key) or []) + actual = set(getattr(ctx, attr)) + undeclared = sorted(actual - declared) + if undeclared: + check( + f"declared {kind}", + False, + f"undeclared {kind} registered (not in {manifest_key}): " + + ", ".join(undeclared), + ) + else: + check(f"declared {kind}", True, "matches registrations") + unregistered = sorted(declared - actual) + if unregistered: + warn( + f"{manifest_key} declares {', '.join(unregistered)} " + "but register() did not register them" + ) + finally: + sys.modules.pop("local_validate_probe_plugin", None) + for key, mod in saved.items(): + if mod is None: + sys.modules.pop(key, None) + else: + sys.modules[key] = mod + + +def main() -> int: + parser = argparse.ArgumentParser(description="Local plugin validation") + parser.add_argument("--path", default=".", help="plugin directory") + args = parser.parse_args() + plugin_dir = Path(args.path).resolve() + if not plugin_dir.is_dir(): + check("plugin directory", False, f"{plugin_dir} is not a directory") + return 1 + manifest_file = plugin_dir / "plugin.yaml" + if not manifest_file.is_file(): + manifest_file = plugin_dir / "plugin.yml" + if not manifest_file.is_file(): + check("manifest", False, "no plugin.yaml in the plugin directory") + return 1 + if yaml is None: + check("manifest", False, "pyyaml not installed (CI must pip install pyyaml)") + return 1 + try: + manifest = yaml.safe_load(manifest_file.read_text(encoding="utf-8")) or {} + except Exception as exc: # noqa: BLE001 - report, don't crash + check("manifest", False, f"plugin.yaml does not parse: {exc}") + return 1 + if not isinstance(manifest, dict): + check("manifest", False, "plugin.yaml top level must be a mapping") + return 1 + check("manifest", True, "plugin.yaml parses") + _check_manifest(manifest) + _check_stdlib_only(plugin_dir) + for path in sorted(plugin_dir.glob("*.py")): + try: + compile(path.read_text(encoding="utf-8"), str(path), "exec") + except SyntaxError as exc: + check("compile", False, f"{path.name}: {exc}") + return 1 + check("compile", True, "all modules compile") + _check_capability_probe(plugin_dir, manifest) + if _failures: + print(f"\nFAIL: {len(_failures)} check(s) failed") + return 1 + print("\nPASS: plugin validated cleanly (local validator)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22ec988..5c31d4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,15 +22,20 @@ jobs: validate: runs-on: ubuntu-latest - # The official action pip-installs hermes-agent from git, which its own - # setup.py build guard refuses (pip/PyPI are unsupported distribution - # methods upstream). Keep attempting (self-heals if upstream fixes the - # action) but don't fail the build over it. - continue-on-error: true + # Local equivalent of the upstream plugin-validate action. The official + # action pip-installs hermes-agent from git, which hermes-agent's own + # setup.py build guard rejects (pip/PyPI are unsupported distribution + # methods upstream), and the action has a single revision in history so + # there is no last-good version to pin. This job runs the same admission + # checks (manifest schema, stdlib-only imports, capability probe) via a + # checked-in script instead, with no hermes-agent install and no + # dependency on an upstream fix. steps: # pinned: actions/checkout@v4 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - # pinned: NousResearch/hermes-agent@main (2026-09-15) - - uses: NousResearch/hermes-agent/.github/actions/plugin-validate@69fd61b0efbe2bf7f412714ed8c35e40dfddc534 + # pinned: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: - path: . + python-version: "3.11" + - run: pip install pyyaml + - run: python .github/scripts/validate-plugin.py --path . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 457d0e1..0d2feea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +12,10 @@ Run the unit suite before opening a pull request: python -m pytest -q ``` -CI also runs the upstream `plugin-validate` step in non-blocking mode (the -upstream pip guard rejects it; the failure reason is documented in `ci.yml`). +CI also runs the local plugin validator (`.github/scripts/validate-plugin.py`), +which mirrors the upstream `plugin-validate` admission checks without installing +hermes-agent (the upstream action's pip install is rejected by hermes-agent's own +setup.py guard, and the action has no last-good version to pin; see `ci.yml`). ## Pull requests