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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install and test
working-directory: hermes/keeperhub
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
Expand Down
24 changes: 10 additions & 14 deletions .github/workflows/release-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
name: release-pypi

# Publishes hermes-plugin-keeperhub (hermes/keeperhub) to PyPI via trusted
# publishing (OIDC). Triggered by a tag, e.g.:
# git tag py-hermes-v1.0.0 && git push origin py-hermes-v1.0.0
# Publishes hermes-plugin-keeperhub to PyPI via trusted publishing (OIDC).
# Triggered by a version tag, e.g.:
# git tag v1.0.0 && git push origin v1.0.0
#
# ONE-TIME SETUP (PyPI supports pending publishers — do this BEFORE first release):
# On pypi.org: Your projects -> Publishing -> Add a pending publisher ->
# PyPI project name: hermes-plugin-keeperhub
# Owner: KeeperHub Repository: agent-plugins
# Owner: KeeperHub Repository: hermes-plugin-keeperhub
# Workflow name: release-pypi.yml Environment: pypi
# Create the matching `pypi` environment under repo Settings -> Environments.

on:
push:
tags:
- "py-hermes-v*"
- "v*"
workflow_dispatch:

permissions:
Expand All @@ -26,9 +26,9 @@ concurrency:

jobs:
publish:
# Only publish from a py-hermes-v* tag. A manual workflow_dispatch (which can
# run from any branch) must not push a release to PyPI.
if: startsWith(github.ref, 'refs/tags/py-hermes-v')
# Only publish from a v* tag. A manual workflow_dispatch (which can run from
# any branch) must not push a release to PyPI.
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: pypi
permissions:
Expand All @@ -39,23 +39,19 @@ jobs:
with:
python-version: "3.12"
- name: Verify tag matches pyproject version
working-directory: hermes/keeperhub
env:
REF_NAME: ${{ github.ref_name }}
run: |
tag="${REF_NAME#py-hermes-v}"
tag="${REF_NAME#v}"
ver="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
echo "tag=$tag pyproject=$ver"
if [ "$tag" != "$ver" ]; then
echo "::error::tag py-hermes-v$tag does not match pyproject version $ver"
echo "::error::tag v$tag does not match pyproject version $ver"
exit 1
fi
- name: Build sdist and wheel
working-directory: hermes/keeperhub
run: |
python -m pip install --upgrade build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: hermes/keeperhub/dist
83 changes: 68 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,80 @@
# KeeperHub agent plugins
# hermes-plugin-keeperhub

Framework plugins that connect AI agent frameworks to
[KeeperHub](https://keeperhub.com) on-chain workflow automation. Each plugin
ships from its own subdirectory and publishes to that framework's registry.
A [Hermes](https://github.com/NousResearch/hermes-agent) agent plugin for
[KeeperHub](https://keeperhub.com) — gives your agent `kh_*` tools to manage and
run on-chain automation workflows, browse templates and protocol actions, and
(opt-in) execute transactions, all over the KeeperHub MCP API.

| Plugin | Framework | Path | Registry |
| --- | --- | --- | --- |
| `hermes-plugin-keeperhub` | [Hermes](https://github.com/NousResearch/hermes) | [`hermes/keeperhub`](./hermes/keeperhub) | PyPI |
## Install

Looking for the framework-agnostic MCP client foundation these build on? See
[`KeeperHub/mcp`](https://github.com/KeeperHub/mcp). For Claude Code, see
[`KeeperHub/claude-plugins`](https://github.com/KeeperHub/claude-plugins).
**Recommended — Hermes plugin manager (no pip needed):**

## Releasing
```bash
hermes plugins install KeeperHub/hermes-plugin-keeperhub --enable
```

**Or via pip / PyPI:**

```bash
pip install hermes-plugin-keeperhub
```
then enable it in your Hermes profile `~/.hermes/config.yaml`:
```yaml
plugins:
enabled:
- keeperhub
```

Each plugin is versioned and tagged independently. Hermes / KeeperHub:
Either way, set your KeeperHub **organization** API key (prefix `kh_`, from
Settings → API Keys → Organisation) and restart Hermes:

```bash
git tag py-hermes-v1.0.0 && git push origin py-hermes-v1.0.0
export KH_API_KEY="kh_..."
```

publishes `hermes/keeperhub` to PyPI via OIDC trusted publishing
(`.github/workflows/release-pypi.yml`).
The plugin's only dependency is `httpx`, which ships with Hermes — so the
clone-based install needs nothing extra.

## Safety: read-only by default

By default the plugin registers **read-only** tools (list/get/search workflows,
executions, templates, integrations, action schemas, status). Tools that change
organization state or move funds on-chain are **withheld** until you opt in:

```bash
export KEEPERHUB_ENABLE_WRITES=true
```

The gate is structural — withheld tools are never registered, so the agent can
neither call nor be delegated a tool that does not exist.

| Mode | Tools |
| --- | --- |
| Default (read-only) | `kh_list_workflows`, `kh_get_workflow`, `kh_search_org_workflows`, `kh_search_workflows_marketplace`, `kh_get_execution_status`, `kh_get_execution_logs`, `kh_get_direct_execution_status`, `kh_search_templates`, `kh_get_template`, `kh_search_plugins`, `kh_get_plugin`, `kh_list_action_schemas`, `kh_search_protocol_actions`, `kh_list_integrations`, `kh_get_wallet_integration`, `kh_ai_generate_workflow`, `kh_tools_documentation`, `kh_status` |
| `KEEPERHUB_ENABLE_WRITES=true` adds | `kh_create_workflow`, `kh_update_workflow`, `kh_delete_workflow`, `kh_execute_workflow`, `kh_deploy_template`, `kh_call_workflow`, `kh_execute_protocol_action`, `kh_execute_transfer`, `kh_execute_contract_call`, `kh_execute_check_and_execute` |

## Try it

Ask your agent things like:

- "List my KeeperHub workflows"
- "Show me workflow `<id>`"
- "What action schemas and chains does KeeperHub support?"
- "Check my KeeperHub connection status" → runs `kh_status`

## Configuration

| Env var | Required | Description |
| --- | --- | --- |
| `KH_API_KEY` | yes | KeeperHub organization API key (`kh_…`). |
| `KEEPERHUB_ENABLE_WRITES` | no | Set to `true`/`1`/`yes`/`on` to register write/exec tools. Default off. |

## Development

```bash
pip install -e ".[dev]"
pytest -q
```

## License

Expand Down
21 changes: 21 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Directory-plugin entry point (no-pip install path).

When installed via `hermes plugins install KeeperHub/hermes-plugin-keeperhub`,
Hermes clones this repo into ~/.hermes/plugins/keeperhub/ and loads THIS file as
the plugin module, reading `register` off it. The relative import resolves the
bundled `keeperhub_plugin` subpackage from the cloned directory — no pip, no
sys.path entry required. (The pip/PyPI path uses the `keeperhub_plugin.entry`
console entry point instead and never imports this file.)
"""

try:
# Hermes directory-load: this file is executed with a synthetic parent
# package (hermes_plugins.keeperhub) + __path__, so the relative import
# resolves the bundled subpackage from the cloned directory — no pip.
from .keeperhub_plugin.entry import register
except ImportError:
# Imported standalone (e.g. a test collector, or keeperhub_plugin already on
# sys.path) where there is no parent package for the relative form.
from keeperhub_plugin.entry import register

__all__ = ["register"]
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The repo-root ./__init__.py is the Hermes directory-plugin entry point — a
# relative-import module that Hermes loads with a synthetic parent package
# (hermes_plugins.keeperhub). It is NOT a test module; if pytest collects/imports
# it directly the relative import raises "attempted relative import with no known
# parent package". Exclude it from collection.
collect_ignore = ["__init__.py"]
Loading
Loading