diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..24a3bb8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,73 @@ +name: Bug report +description: Report reproducible behavior in the Hermes Chutes provider +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve this independent, unofficial integration. + Remove API 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.1.5 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 API keys, credentials, private prompts, and account data. + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b0fd815..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Report a problem with the Chutes provider plugin -title: "[BUG] " -labels: bug -assignees: '' ---- - -## Description -A clear description of the bug. - -## Steps to Reproduce -1. -2. -3. - -## Expected Behavior -What should happen. - -## Actual Behavior -What actually happens. - -## Environment -- Hermes Agent version: -- Python version: -- OS: -- Plugin version: - -## Logs / Traceback -``` -Paste relevant output here. -``` \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/compatibility.yml b/.github/ISSUE_TEMPLATE/compatibility.yml new file mode 100644 index 0000000..10d2609 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility.yml @@ -0,0 +1,53 @@ +name: Compatibility report +description: Report a compatibility problem with a Hermes or Chutes 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 Chutes API behavior. + Remove API keys, credentials, private prompts, and account data before submitting. + - type: dropdown + id: affected-system + attributes: + label: Affected system + options: + - Hermes Agent + - Chutes API or model catalog + - Both or unclear + validations: + required: true + - type: input + id: plugin-version + attributes: + label: Plugin version or commit + placeholder: v0.1.5 or a commit SHA + validations: + required: true + - type: input + id: upstream-version + attributes: + label: Hermes or Chutes version reference + description: Provide a release, commit, dated API behavior, 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 API keys, credentials, private prompts, and account data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 19b0c86..3b327ae 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Chutes Documentation + - name: Report a security vulnerability + url: https://github.com/TheStreamCode/hermes-chutes-provider/security/advisories/new + about: Send sensitive findings privately. Never include credentials in a public issue. + - name: Chutes documentation url: https://docs.chutes.ai - about: Official Chutes platform documentation \ No newline at end of file + about: Consult official platform documentation; it does not support this unofficial integration. + - 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.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8d3b8c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: Feature request +description: Suggest a focused improvement to the provider profile +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + This repository is limited to the standalone Chutes provider profile for Hermes Agent. + Requests that require a custom transport or a Hermes core patch belong upstream. + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What limitation are you trying to address? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed improvement + description: Describe the smallest provider-level change that would help. + validations: + required: true + - type: textarea + id: compatibility + attributes: + label: Compatibility considerations + description: Note any Hermes or Chutes version constraints and link authoritative sources. + - type: checkboxes + id: scope + attributes: + label: Scope check + options: + - label: This request can be implemented without patching Hermes Agent. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e75181a..144026b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,7 @@ ## Validation - [ ] `python -m unittest discover -s tests -v` +- [ ] Hermes integration passed locally or will be covered by CI - [ ] Wheel build completed when packaging changed ## Compatibility and Security diff --git a/.github/assets/github-social-preview.jpg b/.github/assets/github-social-preview.jpg new file mode 100644 index 0000000..0b542f7 Binary files /dev/null and b/.github/assets/github-social-preview.jpg differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1df9b2a..5aa76cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,29 +11,35 @@ permissions: jobs: offline-tests: runs-on: ubuntu-latest + timeout-minutes: 10 strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - run: python -m unittest discover -s tests -v hermes-integration: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: provider - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: NousResearch/hermes-agent - ref: 46e87b14fd6c943ef0d6671fb0d74c5dde5d4c6b + ref: 3c27eb6234bf91b8ceee9e9071591b31e9b148cb # v2026.8.3 path: hermes-agent - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - run: python -m pip install --disable-pip-version-check -e hermes-agent diff --git a/AGENTS.md b/AGENTS.md index 5a2fd5b..91a733a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ framing intact in every document you touch. | Build backend | `setuptools>=77` via `pyproject.toml` | | Package manager | `pip` (CI uses `python -m pip`); `uv` is fine locally but do not add a second lockfile or package manager to the repo | | Lockfile | none, and none is needed while `dependencies = []` | -| CI | GitHub Actions, `.github/workflows/ci.yml`, plus CodeQL default setup | +| CI | GitHub Actions, `.github/workflows/ci.yml` (3.11/3.12/3.13/3.14), plus CodeQL default setup | There is no lint, format, or type-check step configured. Do not introduce one as a drive-by change; the codebase is small and has a consistent internal style @@ -121,6 +121,7 @@ Semantic versioning. A version bump must update **all** of these together: - `plugin.yaml` `version` - `hermes_chutes_provider/__init__.py` `__version__` - `CITATION.cff` `version` and `date-released` +- the released tag in the README manual-install commands - a new `CHANGELOG.md` section with the real release date - `RELEASE_VERSION` and `RELEASE_DATE` in `tests/test_plugin_profile.py` @@ -129,7 +130,8 @@ Semantic versioning. A version bump must update **all** of these together: ## Release procedure `main` is protected: linear history, no force pushes, required status checks -`offline-tests (3.11)` and `offline-tests (3.13)`, and **one approving review**. +`offline-tests (3.11)`, `offline-tests (3.14)`, `hermes-integration`, and +`CodeQL`, enforced for administrators, plus **one approving review**. Therefore: 1. Work on a branch, never directly on `main`. diff --git a/CHANGELOG.md b/CHANGELOG.md index f6bbb7a..bda12b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to this project are documented in this file. +## [0.1.5] - 2026-08-09 + +### Security + +- Bound live catalog responses to 8 MiB before decoding, so a misbehaving + endpoint cannot make the provider buffer an unbounded response. +- Stop persisting GitHub credentials in CI checkouts and enforce commit-pinned + Actions through the offline contract. + +### Changed + +- Reject malformed catalog payload shapes while preserving an empty valid + catalog and the routing-alias fallback behavior. +- Test Python 3.14 and refresh the pinned GitHub Actions and Hermes integration + revision to currently verified releases. +- Install the released tag by default and document `main` as a development + checkout. +- Present the current support and distribution status before installation + details and document the provider's relationship to Hermes' generic + transport. + +### Added + +- Regression coverage for older Hermes provider profiles, bounded catalog + reads, optional request metadata, and workflow pinning. +- Structured bug, compatibility, and feature-request forms with explicit + credential-redaction guidance and a private security-reporting route. + ## [0.1.4] - 2026-08-02 ### Fixed diff --git a/CITATION.cff b/CITATION.cff index 68e8226..e7f84dd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,8 +1,8 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." title: "Hermes Chutes Provider" -version: 0.1.4 -date-released: "2026-08-02" +version: 0.1.5 +date-released: "2026-08-09" url: "https://github.com/TheStreamCode/hermes-chutes-provider" repository-code: "https://github.com/TheStreamCode/hermes-chutes-provider" license: MIT diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dec0efd..882fda9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,12 +19,21 @@ Run the offline test suite before opening a pull request: python -m unittest discover -s tests -v ``` -Changes to packaging should also build a wheel successfully: +When a Hermes checkout is available, run the integration contract as well: + +```bash +HERMES_SOURCE=/path/to/hermes-agent python -m unittest discover -s tests -v +``` + +Changes to packaging should build both release distributions and the CI wheel: ```bash python -m pip wheel . --no-deps +python -m build ``` +No validation command may perform paid inference or use a real API key. + ## Pull Requests Explain the behavior changed, the files affected, how the change was validated, diff --git a/README.md b/README.md index ff72c5c..550beae 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,20 @@ not need an upstream Hermes source change. This is an independent, unofficial integration. It is not affiliated with, endorsed by, sponsored by, or approved by Chutes Global Corp or Nous Research. +## Project status + +| Area | Current status | +| --- | --- | +| Stable release | [`v0.1.5`](https://github.com/TheStreamCode/hermes-chutes-provider/releases/tag/v0.1.5) | +| Supported installation | Manual directory install from the released tag | +| Native Hermes install | Pending upstream standalone-plugin support in [Hermes PR #64277](https://github.com/NousResearch/hermes-agent/pull/64277) | +| Python | 3.11 or newer | +| Distribution | GitHub Releases only; not published to PyPI | + +The profile keeps the stable `chutes` provider identity, discovers current +tool-capable models from the live Chutes catalog, and falls back to Chutes' +routing aliases when that catalog cannot be reached. + ## Requirements - A Hermes Agent installation with model-provider plugin discovery. @@ -25,12 +39,13 @@ endorsed by, sponsored by, or approved by Chutes Global Corp or Nous Research. ### Manual directory install -Clone this repository into the active Hermes profile: +Clone the latest released snapshot into the active Hermes profile: ```bash export HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" mkdir -p "$HERMES_HOME/plugins/model-providers" -git clone https://github.com/TheStreamCode/hermes-chutes-provider.git \ +git clone --branch v0.1.5 --depth 1 \ + https://github.com/TheStreamCode/hermes-chutes-provider.git \ "$HERMES_HOME/plugins/model-providers/chutes" ``` @@ -40,9 +55,15 @@ On PowerShell: $hermesHome = if ($env:HERMES_HOME) { $env:HERMES_HOME } else { Join-Path $HOME ".hermes" } $target = Join-Path $hermesHome "plugins\model-providers\chutes" New-Item -ItemType Directory -Force (Split-Path -Parent $target) -git clone https://github.com/TheStreamCode/hermes-chutes-provider.git $target +git clone --branch v0.1.5 --depth 1 https://github.com/TheStreamCode/hermes-chutes-provider.git $target ``` +#### Development checkout + +To test unreleased changes, clone `main` instead by omitting `--branch` and +`--depth`. Treat that checkout as development code rather than a published +release. + Add the key to `$HERMES_HOME/.env`: ```dotenv @@ -119,6 +140,13 @@ This plugin declares metadata for Hermes' existing generic OpenAI-compatible transport and provider-discovery systems. It does not modify Hermes Agent, add a custom transport, or include Chutes API keys. +```mermaid +flowchart LR + P["Chutes provider profile"] -->|configures| R["Hermes provider registry"] + R --> T["Generic OpenAI-compatible transport"] + T --> C["Chutes API"] +``` + For Chutes skills, setup recipes, model guidance, and platform documentation, see [Veightor/chutes-agent-toolkit](https://github.com/Veightor/chutes-agent-toolkit). @@ -162,9 +190,10 @@ integration contract red. ## Versioning and releases The project follows semantic versioning. `pyproject.toml`, `plugin.yaml`, -`CITATION.cff`, `hermes_chutes_provider.__version__`, and `CHANGELOG.md` must -carry the same version, while `CHANGELOG.md` and `CITATION.cff` must also share -the real release date; `test_release_metadata_is_consistent` enforces both. +`CITATION.cff`, `hermes_chutes_provider.__version__`, `CHANGELOG.md`, and the +manual-install tag above must carry the same version, while `CHANGELOG.md` and +`CITATION.cff` must also share the real release date; +`test_release_metadata_is_consistent` enforces both. Published versions are listed under [Releases](https://github.com/TheStreamCode/hermes-chutes-provider/releases). diff --git a/SECURITY.md b/SECURITY.md index 9cfd7f0..22b4651 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,12 @@ ## Reporting a Vulnerability -Report sensitive vulnerabilities privately to `info@mikesoft.it` with the -subject `Hermes Chutes Provider Security Report`. Do not disclose API keys, -credentials, private prompts, or account data in a public issue. +Use GitHub's +[private vulnerability reporting](https://github.com/TheStreamCode/hermes-chutes-provider/security/advisories/new) +for sensitive findings. If that route is unavailable, email +`info@mikesoft.it` with the subject `Hermes Chutes Provider 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. diff --git a/hermes_chutes_provider/__init__.py b/hermes_chutes_provider/__init__.py index 0c7e236..d5a9663 100644 --- a/hermes_chutes_provider/__init__.py +++ b/hermes_chutes_provider/__init__.py @@ -8,8 +8,9 @@ from providers.base import ProviderProfile -__version__ = "0.1.4" +__version__ = "0.1.5" logger = logging.getLogger(__name__) +_MAX_CATALOG_RESPONSE_BYTES = 8 * 1024 * 1024 class ChutesProviderProfile(ProviderProfile): @@ -51,8 +52,18 @@ def fetch_model_metadata( request.add_header(key, value) with open_credentialed_url(request, timeout=timeout) as response: - payload = json.loads(response.read().decode()) - items = payload if isinstance(payload, list) else payload.get("data", []) + body = response.read(_MAX_CATALOG_RESPONSE_BYTES + 1) + if len(body) > _MAX_CATALOG_RESPONSE_BYTES: + return None + + payload = json.loads(body.decode("utf-8")) + if isinstance(payload, list): + items = payload + elif isinstance(payload, dict) and isinstance(payload.get("data"), list): + items = payload["data"] + else: + return None + models = [] for item in items: if not isinstance(item, dict): diff --git a/plugin.yaml b/plugin.yaml index 560980f..a082ff2 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,6 +1,6 @@ name: chutes-provider kind: model-provider -version: 0.1.4 +version: 0.1.5 description: Chutes model provider for Hermes Agent author: Michael Gasperini (Mikesoft) requires_env: diff --git a/pyproject.toml b/pyproject.toml index 501c837..5d75592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hermes-chutes-provider" -version = "0.1.4" +version = "0.1.5" description = "Chutes model-provider plugin for Hermes Agent" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_plugin_profile.py b/tests/test_plugin_profile.py index 6c0d6b5..6c678c4 100644 --- a/tests/test_plugin_profile.py +++ b/tests/test_plugin_profile.py @@ -2,8 +2,10 @@ from __future__ import annotations +from contextlib import contextmanager import importlib.util from pathlib import Path +import re import sys import types import unittest @@ -13,13 +15,17 @@ PLUGIN_PATH = REPOSITORY_ROOT / "__init__.py" README_PATH = REPOSITORY_ROOT / "README.md" CI_WORKFLOW_PATH = REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml" +DEPENDABOT_WORKFLOW_PATH = ( + REPOSITORY_ROOT / ".github" / "workflows" / "dependabot-auto-merge.yml" +) PACKAGE_PATH = REPOSITORY_ROOT / "hermes_chutes_provider" / "__init__.py" PYPROJECT_PATH = REPOSITORY_ROOT / "pyproject.toml" MANIFEST_PATH = REPOSITORY_ROOT / "plugin.yaml" CHANGELOG_PATH = REPOSITORY_ROOT / "CHANGELOG.md" CITATION_PATH = REPOSITORY_ROOT / "CITATION.cff" -RELEASE_VERSION = "0.1.4" -RELEASE_DATE = "2026-08-02" +ISSUE_TEMPLATE_DIRECTORY = REPOSITORY_ROOT / ".github" / "ISSUE_TEMPLATE" +RELEASE_VERSION = "0.1.5" +RELEASE_DATE = "2026-08-09" class ProviderProfile: @@ -34,7 +40,9 @@ def __init__(self, **attributes: object) -> None: self.__dict__.update(attributes) -def load_directory_plugin() -> ProviderProfile: +def load_directory_plugin( + profile_type: type[ProviderProfile] = ProviderProfile, +) -> ProviderProfile: """Import the provider from its Hermes user-plugin directory layout.""" if not PLUGIN_PATH.is_file(): @@ -44,7 +52,7 @@ def load_directory_plugin() -> ProviderProfile: providers_module = types.ModuleType("providers") providers_module.register_provider = registered.append base_module = types.ModuleType("providers.base") - base_module.ProviderProfile = ProviderProfile + base_module.ProviderProfile = profile_type module_name = "chutes_directory_plugin_under_test" module_names = ("providers", "providers.base", module_name) @@ -76,10 +84,49 @@ def load_directory_plugin() -> ProviderProfile: return registered[0] +@contextmanager +def stub_catalog_response(payload: bytes): + """Stub Hermes' credentialed opener with a bounded in-memory response.""" + + requests = [] + + class Response: + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def read(self, size: int = -1) -> bytes: + return payload if size < 0 else payload[:size] + + def open_credentialed_url(request, timeout): + requests.append((request, timeout)) + return Response() + + hermes_cli_module = types.ModuleType("hermes_cli") + hermes_cli_module.__path__ = [] + security_module = types.ModuleType("hermes_cli.urllib_security") + security_module.open_credentialed_url = open_credentialed_url + module_names = ("hermes_cli", "hermes_cli.urllib_security") + previous_modules = {name: sys.modules.get(name) for name in module_names} + try: + sys.modules["hermes_cli"] = hermes_cli_module + sys.modules["hermes_cli.urllib_security"] = security_module + yield requests + finally: + for name, previous_module in previous_modules.items(): + if previous_module is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous_module + + class ChutesDirectoryPluginTests(unittest.TestCase): def test_release_metadata_is_consistent(self) -> None: changelog = CHANGELOG_PATH.read_text(encoding="utf-8") citation = CITATION_PATH.read_text(encoding="utf-8") + readme = README_PATH.read_text(encoding="utf-8") self.assertIn( f'__version__ = "{RELEASE_VERSION}"', @@ -105,6 +152,7 @@ def test_release_metadata_is_consistent(self) -> None: f'date-released: "{RELEASE_DATE}"', citation, ) + self.assertIn(f"--branch v{RELEASE_VERSION}", readme) def test_registers_the_chutes_profile(self) -> None: profile = load_directory_plugin() @@ -142,31 +190,7 @@ def test_live_catalog_only_returns_tool_capable_models(self) -> None: {"supported_features": ["tools"]} ] }''' - requests = [] - - class Response: - def __enter__(self): - return self - - def __exit__(self, *_args): - return None - - def read(self) -> bytes: - return payload - - def open_credentialed_url(request, timeout): - requests.append((request, timeout)) - return Response() - - hermes_cli_module = types.ModuleType("hermes_cli") - hermes_cli_module.__path__ = [] - security_module = types.ModuleType("hermes_cli.urllib_security") - security_module.open_credentialed_url = open_credentialed_url - module_names = ("hermes_cli", "hermes_cli.urllib_security") - previous_modules = {name: sys.modules.get(name) for name in module_names} - try: - sys.modules["hermes_cli"] = hermes_cli_module - sys.modules["hermes_cli.urllib_security"] = security_module + with stub_catalog_response(payload) as requests: metadata = profile.fetch_model_metadata( api_key="cpk_test-key", base_url="https://chutes.test/v1", @@ -175,12 +199,6 @@ def open_credentialed_url(request, timeout): api_key="cpk_test-key", base_url="https://chutes.test/v1", ) - finally: - for name, previous_module in previous_modules.items(): - if previous_module is None: - sys.modules.pop(name, None) - else: - sys.modules[name] = previous_module self.assertEqual( metadata, @@ -198,6 +216,77 @@ def open_credentialed_url(request, timeout): self.assertEqual(request.get_header("Authorization"), "Bearer cpk_test-key") self.assertEqual(timeout, 8.0) + def test_catalog_rejects_malformed_or_oversized_payloads(self) -> None: + profile = load_directory_plugin() + malformed_payloads = ( + b"null", + b"{}", + b'{"data": null}', + b'{"data": "not-a-list"}', + ) + + for payload in malformed_payloads: + with self.subTest(payload=payload): + with stub_catalog_response(payload): + self.assertIsNone(profile.fetch_model_metadata()) + + oversized_payload = b" " * (8 * 1024 * 1024 + 1) + with stub_catalog_response(oversized_payload): + self.assertIsNone(profile.fetch_model_metadata()) + + def test_catalog_accepts_empty_list_payloads(self) -> None: + profile = load_directory_plugin() + + for payload in (b"[]", b'{"data": []}'): + with self.subTest(payload=payload): + with stub_catalog_response(payload): + self.assertEqual(profile.fetch_model_metadata(), []) + + def test_catalog_supports_older_provider_profiles(self) -> None: + class LegacyProviderProfile: + fallback_models = () + default_aux_model = "" + + def __init__(self, **attributes: object) -> None: + self.__dict__.update(attributes) + + profile = load_directory_plugin(LegacyProviderProfile) + payload = b'{"data": [{"id": "tool-model", "supported_features": ["tools"]}]}' + + with stub_catalog_response(payload) as requests: + self.assertEqual( + profile.fetch_models( + api_key=None, + base_url="https://legacy-chutes.test/v1", + ), + ["tool-model"], + ) + + request, timeout = requests[0] + self.assertEqual(request.full_url, "https://legacy-chutes.test/v1/models") + self.assertIsNone(request.get_header("Authorization")) + self.assertEqual(timeout, 8.0) + + def test_catalog_honors_explicit_url_and_default_headers(self) -> None: + profile = load_directory_plugin() + profile.models_url = "https://catalog.chutes.test/models" + profile.default_headers = {"X-Catalog-Test": "enabled"} + payload = b'{"data": [{"id": "tool-model", "supported_features": ["TOOLS"]}]}' + + with stub_catalog_response(payload) as requests: + self.assertEqual( + profile.fetch_models( + api_key=None, + base_url="https://ignored.chutes.test/v1", + ), + ["tool-model"], + ) + + request, _timeout = requests[0] + self.assertEqual(request.full_url, "https://catalog.chutes.test/models") + self.assertEqual(request.get_header("X-catalog-test"), "enabled") + self.assertIsNone(request.get_header("Authorization")) + def test_readme_documents_current_and_future_install_paths(self) -> None: if not README_PATH.is_file(): self.fail(f"missing README: {README_PATH}") @@ -216,6 +305,34 @@ def test_readme_documents_current_and_future_install_paths(self) -> None: self.assertIn("model.context_length", readme) self.assertIn("https://github.com/Veightor/chutes-agent-toolkit", readme) self.assertNotIn("https://github.com/chutesai/chutes-agent-toolkit", readme) + self.assertIn("Development checkout", readme) + self.assertIn("## Project status", readme) + self.assertIn("GitHub Releases only; not published to PyPI", readme) + self.assertIn("Generic OpenAI-compatible transport", readme) + + def test_issue_forms_route_security_reports_and_guard_credentials(self) -> None: + config = (ISSUE_TEMPLATE_DIRECTORY / "config.yml").read_text( + encoding="utf-8" + ) + bug_form = (ISSUE_TEMPLATE_DIRECTORY / "bug.yml").read_text( + encoding="utf-8" + ) + compatibility_form = ( + ISSUE_TEMPLATE_DIRECTORY / "compatibility.yml" + ).read_text(encoding="utf-8") + feature_form = (ISSUE_TEMPLATE_DIRECTORY / "feature_request.yml").read_text( + encoding="utf-8" + ) + + self.assertIn("blank_issues_enabled: false", config) + self.assertIn("security/advisories/new", config) + self.assertIn('labels: ["bug"]', bug_form) + self.assertIn('labels: ["compatibility"]', compatibility_form) + self.assertIn('labels: ["enhancement"]', feature_form) + for form in (bug_form, compatibility_form): + with self.subTest(form=form[:40]): + self.assertIn("Remove API keys", form) + self.assertIn("required: true", form) def test_ci_runs_the_offline_contract_suite(self) -> None: if not CI_WORKFLOW_PATH.is_file(): @@ -224,11 +341,34 @@ def test_ci_runs_the_offline_contract_suite(self) -> None: workflow = CI_WORKFLOW_PATH.read_text(encoding="utf-8") self.assertIn("python -m unittest discover -s tests -v", workflow) self.assertIn("NousResearch/hermes-agent", workflow) - self.assertIn("46e87b14fd6c943ef0d6671fb0d74c5dde5d4c6b", workflow) + self.assertIn("3c27eb6234bf91b8ceee9e9071591b31e9b148cb", workflow) + self.assertIn("3d3c42e5aac5ba805825da76410c181273ba90b1", workflow) + self.assertIn("5fda3b95a4ea91299a34e894583c3862153e4b97", workflow) self.assertIn("HERMES_SOURCE", workflow) self.assertIn("python -m pip wheel . --no-deps", workflow) + self.assertIn('python-version: ["3.11", "3.12", "3.13", "3.14"]', workflow) + self.assertEqual(workflow.count("persist-credentials: false"), 3) + self.assertEqual(workflow.count("timeout-minutes:"), 2) self.assertNotIn("CHUTES_API_KEY", workflow) + def test_github_actions_are_pinned_by_commit(self) -> None: + for workflow_path in (CI_WORKFLOW_PATH, DEPENDABOT_WORKFLOW_PATH): + workflow = workflow_path.read_text(encoding="utf-8") + uses_lines = [ + line.strip() + for line in workflow.splitlines() + if re.match(r"^(?:-\s+)?uses:", line.strip()) + ] + self.assertTrue(uses_lines, workflow_path) + for uses_line in uses_lines: + with self.subTest(workflow=workflow_path, uses=uses_line): + self.assertRegex( + uses_line, + re.compile( + r"^(?:-\s+)?uses: [^@\s]+@[0-9a-f]{40}(?:\s+#.*)?$" + ), + ) + if __name__ == "__main__": unittest.main()