From 5c08b851dfcc23508c8e435510d910f09ba8da31 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 7 May 2026 07:36:42 -0700 Subject: [PATCH] docs(platforms): document env_enablement_fn + cron_deliver_env_var hooks (#21331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following PR #21306 which added the new generic plugin-platform hooks, update the three platform-authoring docs so plugin authors find them: - website/docs/developer-guide/adding-platform-adapters.md: expand the 'What the Plugin System Handles Automatically' table with env-only auto-enable + cron delivery + hermes-config UI entries rows. Add three new sections — 'Env-Driven Auto-Configuration', 'Cron Delivery', 'Surfacing Env Vars in hermes config' — covering the hook signatures, plugin.yaml rich-dict format, and the home_channel-key special case. Update the main register() example to pass env_enablement_fn + cron_deliver_env_var inline so readers see them on their first pass. Upgrade the PLUGIN.yaml snippet to show bare-string + rich-dict + optional_env. - website/docs/guides/build-a-hermes-plugin.md: the thin platform example in the build-a-plugin tour now includes env_enablement_fn and cron_deliver_env_var, plus an optional_env block in the inline plugin.yaml. Keeps pointing to the developer-guide page for the full treatment. - gateway/platforms/ADDING_A_PLATFORM.md: the in-repo reference shallow-points at the docsite but now names the three new hooks explicitly so contributors reading the source tree know what they're for. Also adds teams + google_chat as reference implementations alongside irc. --- gateway/platforms/ADDING_A_PLATFORM.md | 28 +++- .../adding-platform-adapters.md | 134 +++++++++++++++++- website/docs/guides/build-a-hermes-plugin.md | 22 ++- 3 files changed, 174 insertions(+), 10 deletions(-) diff --git a/gateway/platforms/ADDING_A_PLATFORM.md b/gateway/platforms/ADDING_A_PLATFORM.md index 7fd28245b1..5091c4647c 100644 --- a/gateway/platforms/ADDING_A_PLATFORM.md +++ b/gateway/platforms/ADDING_A_PLATFORM.md @@ -4,18 +4,34 @@ There are two ways to add a platform to the Hermes gateway: ## Plugin Path (Recommended for Community/Third-Party) -Create a plugin directory in `~/.hermes/plugins/` with a `PLUGIN.yaml` and -`adapter.py`. The adapter inherits from `BasePlatformAdapter` and registers -via `ctx.register_platform()` in the `register(ctx)` entry point. This -requires **zero changes to core Hermes code**. +Create a plugin directory in `~/.hermes/plugins/` (or under `plugins/platforms/` +for bundled plugins) with a `plugin.yaml` and `adapter.py`. The adapter +inherits from `BasePlatformAdapter` and registers via +`ctx.register_platform()` in the `register(ctx)` entry point. This requires +**zero changes to core Hermes code**. The plugin system automatically handles: adapter creation, config parsing, user authorization, cron delivery, send_message routing, system prompt hints, status display, gateway setup, and more. -See `plugins/platforms/irc/` for a complete reference implementation, and +**Three optional hooks cover the edges most adapters need:** + +- `env_enablement_fn: () -> Optional[dict]` — seeds `PlatformConfig.extra` + (and an optional `home_channel` dict) from env vars BEFORE the adapter is + constructed. Without this, env-only setups don't surface in + `hermes gateway status` or `get_connected_platforms()` until the SDK + instantiates. +- `cron_deliver_env_var: str` — name of the `*_HOME_CHANNEL` env var. When + set, `deliver=` cron jobs route to this var without editing + `cron/scheduler.py`'s hardcoded sets. +- `plugin.yaml` `requires_env` / `optional_env` rich-dict entries — + auto-populate `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` so the setup + wizard surfaces proper descriptions, prompts, password flags, and URLs. + +See `plugins/platforms/irc/`, `plugins/platforms/teams/`, and +`plugins/platforms/google_chat/` for complete working examples, and `website/docs/developer-guide/adding-platform-adapters.md` for the full -plugin guide with code examples. +plugin guide with code examples and hook documentation. --- diff --git a/website/docs/developer-guide/adding-platform-adapters.md b/website/docs/developer-guide/adding-platform-adapters.md index 5bab2fc4be..763f9e6d1f 100644 --- a/website/docs/developer-guide/adding-platform-adapters.md +++ b/website/docs/developer-guide/adding-platform-adapters.md @@ -40,13 +40,25 @@ The plugin system lets you add a platform adapter without modifying any core Her ### PLUGIN.yaml +Plugin metadata. The `requires_env` and `optional_env` blocks auto-populate `hermes config` UI entries (see [Surfacing Env Vars](#surfacing-env-vars-in-hermes-config) below). + ```yaml name: my-platform +label: My Platform +kind: platform version: 1.0.0 description: My custom messaging platform adapter +author: Your Name requires_env: - - MY_PLATFORM_TOKEN - - MY_PLATFORM_CHANNEL + - MY_PLATFORM_TOKEN # bare string works + - name: MY_PLATFORM_CHANNEL # or rich dict for better UX + description: "Channel to join" + prompt: "Channel" + password: false +optional_env: + - name: MY_PLATFORM_HOME_CHANNEL + description: "Default channel for cron delivery" + password: false ``` ### adapter.py @@ -90,6 +102,18 @@ def validate_config(config) -> bool: return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token")) +def _env_enablement() -> dict | None: + token = os.getenv("MY_PLATFORM_TOKEN", "").strip() + channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip() + if not (token and channel): + return None + seed = {"token": token, "channel": channel} + home = os.getenv("MY_PLATFORM_HOME_CHANNEL") + if home: + seed["home_channel"] = {"chat_id": home, "name": "Home"} + return seed + + def register(ctx): """Plugin entry point — called by the Hermes plugin system.""" ctx.register_platform( @@ -100,6 +124,14 @@ def register(ctx): validate_config=validate_config, required_env=["MY_PLATFORM_TOKEN"], install_hint="pip install my-platform-sdk", + # Env-driven auto-configuration — seeds PlatformConfig.extra from + # env vars before adapter construction. See "Env-Driven Auto- + # Configuration" section below. + env_enablement_fn=_env_enablement, + # Cron home-channel delivery support. Lets deliver=my_platform cron + # jobs route without editing cron/scheduler.py. See "Cron Delivery" + # section below. + cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL", # Per-platform user authorization env vars allowed_users_env="MY_PLATFORM_ALLOWED_USERS", allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS", @@ -149,7 +181,9 @@ When you call `ctx.register_platform()`, the following integration points are ha | Config parsing | `Platform._missing_()` accepts any platform name | | Connected platform validation | Registry `validate_config()` called | | User authorization | `allowed_users_env` / `allow_all_env` checked | -| Cron delivery | `Platform()` resolves any registered name | +| Env-only auto-enable | `env_enablement_fn` seeds `PlatformConfig.extra` + `home_channel` | +| Cron delivery | `cron_deliver_env_var` makes `deliver=` work | +| `hermes config` UI entries | `requires_env` / `optional_env` in `plugin.yaml` auto-populate | | send_message tool | Routes through live gateway adapter | | Webhook cross-platform delivery | Registry checked for known platforms | | `/update` command access | `allow_update_command` flag | @@ -163,6 +197,100 @@ When you call `ctx.register_platform()`, the following integration points are ha | Token lock (multi-profile) | Use `acquire_scoped_lock()` in your `connect()` | | Orphaned config warning | Descriptive log when plugin is missing | +## Env-Driven Auto-Configuration + +Most users set up a platform by dropping env vars into `~/.hermes/.env` rather than editing `config.yaml`. The `env_enablement_fn` hook lets your plugin pick those env vars up **before** the adapter is constructed, so `hermes gateway status`, `get_connected_platforms()`, and cron delivery see the correct state without instantiating the platform SDK. + +```python +def _env_enablement() -> dict | None: + """Seed PlatformConfig.extra from env vars. + + Called by the platform registry during load_gateway_config(). + Return None when the platform isn't minimally configured — the + caller then skips auto-enabling. Return a dict to seed extras. + + The special 'home_channel' key is extracted and becomes a proper + HomeChannel dataclass on the PlatformConfig; every other key is + merged into PlatformConfig.extra. + """ + token = os.getenv("MY_PLATFORM_TOKEN", "").strip() + channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip() + if not (token and channel): + return None + seed = {"token": token, "channel": channel} + home = os.getenv("MY_PLATFORM_HOME_CHANNEL") + if home: + seed["home_channel"] = { + "chat_id": home, + "name": os.getenv("MY_PLATFORM_HOME_CHANNEL_NAME", "Home"), + } + return seed + + +def register(ctx): + ctx.register_platform( + name="my_platform", + label="My Platform", + adapter_factory=lambda cfg: MyPlatformAdapter(cfg), + check_fn=check_requirements, + validate_config=validate_config, + env_enablement_fn=_env_enablement, + # ... other fields + ) +``` + +## Cron Delivery + +To let `deliver=my_platform` cron jobs route to a configured home channel, set `cron_deliver_env_var` to the env var name that holds the default chat/room/channel ID: + +```python +ctx.register_platform( + name="my_platform", + ... + cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL", +) +``` + +The scheduler reads this env var when resolving the home target for `deliver=my_platform` jobs, and also treats the platform as a valid cron target in `_KNOWN_DELIVERY_PLATFORMS`-style checks. If your `env_enablement_fn` seeds a `home_channel` dict (see above), that takes precedence — `cron_deliver_env_var` is the fallback for cron jobs that run before env seeding. + +## Surfacing Env Vars in `hermes config` + +`hermes_cli/config.py` scans `plugins/platforms/*/plugin.yaml` at import time and auto-populates `OPTIONAL_ENV_VARS` from `requires_env` and (optional) `optional_env` blocks. Use the rich-dict form to contribute proper descriptions, prompts, password flags, and URLs — the CLI setup UI picks them up for free. + +```yaml +# plugins/platforms/my_platform/plugin.yaml +name: my_platform-platform +label: My Platform +kind: platform +version: 1.0.0 +description: > + My Platform gateway adapter for Hermes Agent. +author: Your Name +requires_env: + - name: MY_PLATFORM_TOKEN + description: "Bot API token from the My Platform console" + prompt: "My Platform bot token" + url: "https://my-platform.example.com/bots" + password: true + - name: MY_PLATFORM_CHANNEL + description: "Channel to join (e.g. #hermes)" + prompt: "Channel" + password: false +optional_env: + - name: MY_PLATFORM_HOME_CHANNEL + description: "Default channel for cron delivery (defaults to MY_PLATFORM_CHANNEL)" + prompt: "Home channel (or empty)" + password: false + - name: MY_PLATFORM_ALLOWED_USERS + description: "Comma-separated user IDs allowed to talk to the bot" + prompt: "Allowed users (comma-separated)" + password: false +``` + +**Supported dict keys:** `name` (required), `description`, `prompt`, `url`, `password` (bool; auto-detected from `*_TOKEN` / `*_SECRET` / `*_KEY` / `*_PASSWORD` / `*_JSON` suffix when omitted), `category` (defaults to `"messaging"`). + +Bare-string entries (`- MY_PLATFORM_TOKEN`) still work — they get a generic description auto-derived from the plugin's `label`. If a hardcoded entry for the same var already exists in `OPTIONAL_ENV_VARS`, it wins (back-compat); the plugin.yaml form acts as the fallback. + ### Reference Implementation See `plugins/platforms/irc/` in the repo for a complete working example — a full async IRC adapter with zero external dependencies. diff --git a/website/docs/guides/build-a-hermes-plugin.md b/website/docs/guides/build-a-hermes-plugin.md index 881d0a4cc3..748bc18564 100644 --- a/website/docs/guides/build-a-hermes-plugin.md +++ b/website/docs/guides/build-a-hermes-plugin.md @@ -747,6 +747,13 @@ def check_requirements(): import os return bool(os.environ.get("MYPLATFORM_TOKEN")) +def _env_enablement(): + import os + tok = os.getenv("MYPLATFORM_TOKEN", "").strip() + if not tok: + return None + return {"token": tok} + def register(ctx): ctx.register_platform( name="myplatform", @@ -754,6 +761,11 @@ def register(ctx): adapter_factory=lambda cfg: MyPlatformAdapter(cfg), check_fn=check_requirements, required_env=["MYPLATFORM_TOKEN"], + # Auto-populate PlatformConfig.extra from env so env-only setups + # show up in `hermes gateway status` without SDK instantiation. + env_enablement_fn=_env_enablement, + # Opt in to cron delivery: `deliver=myplatform` routes to this var. + cron_deliver_env_var="MYPLATFORM_HOME_CHANNEL", emoji="💬", platform_hint="You are chatting via MyPlatform. Keep responses concise.", ) @@ -762,10 +774,18 @@ def register(ctx): ```yaml # plugins/platforms/myplatform/plugin.yaml name: myplatform-platform +label: MyPlatform kind: platform version: 1.0.0 description: MyPlatform gateway adapter -requires_env: [MYPLATFORM_TOKEN] +requires_env: + - name: MYPLATFORM_TOKEN + description: "Bot token from the MyPlatform console" + password: true +optional_env: + - name: MYPLATFORM_HOME_CHANNEL + description: "Default channel for cron delivery" + password: false ``` **Full guide:** [Adding Platform Adapters](/docs/developer-guide/adding-platform-adapters) — complete `BasePlatformAdapter` contract, message routing, auth gating, setup wizard integration. Look at `plugins/platforms/irc/` for a stdlib-only working example.