refactor(plugins/platforms): migrate IRC + Teams to new env_enablement + cron_deliver hooks

Adopt the generic platform-plugin hooks landed in the preceding commit
so IRC and Teams get env-only config detection and cron home-channel
delivery without living in cron/scheduler.py's hardcoded sets.

IRC (plugins/platforms/irc/):
- adapter.py: new _env_enablement() seeds server, channel, port,
  nickname, use_tls, server_password, nickserv_password, and a
  home_channel dict into PlatformConfig on env-only setups.
  IRC_HOME_CHANNEL defaults to IRC_CHANNEL so deliver=irc cron jobs
  route to the joined channel by default.
- adapter.py: register_platform() gains env_enablement_fn=_env_enablement
  and cron_deliver_env_var='IRC_HOME_CHANNEL'.
- plugin.yaml: rich requires_env / optional_env with description,
  prompt, password, url for every IRC env var.  Hardcoded IRC entries
  in hermes_cli/config.py still win (back-compat), but the plugin now
  carries its own metadata.

Teams (plugins/platforms/teams/):
- adapter.py: new _env_enablement() seeds client_id, client_secret,
  tenant_id, port, and home_channel into PlatformConfig.  Closes the
  long-standing gap where TEAMS_HOME_CHANNEL was documented but never
  wired up.
- adapter.py: register_platform() gains env_enablement_fn=_env_enablement
  and cron_deliver_env_var='TEAMS_HOME_CHANNEL' — deliver=teams cron
  jobs now work.
- plugin.yaml: rich requires_env / optional_env with description,
  prompt, password, url for every Teams env var.  Surfaces them in
  'hermes config' UI for the first time (Teams had no OPTIONAL_ENV_VARS
  entries before this).

Zero behavior change for existing users: env_enablement_fn is only
called when env vars are set, and the registry's config-first-env-fallback
path in validate_config / is_connected is unchanged.
This commit is contained in:
Teknium 2026-05-07 06:47:25 -07:00
parent 44cd79e798
commit be87a96296
4 changed files with 185 additions and 6 deletions

View file

@ -653,6 +653,57 @@ def is_connected(config) -> bool:
return bool(server and channel) return bool(server and channel)
def _env_enablement() -> dict | None:
"""Seed ``PlatformConfig.extra`` from env vars during gateway config load.
Called by the platform registry's env-enablement hook (landed in the
generic-plugin-interface migration) BEFORE adapter construction, so
``gateway status`` and ``get_connected_platforms()`` reflect env-only
configuration without instantiating the IRC client. Returns ``None``
when IRC isn't minimally configured; the caller skips auto-enabling.
The special ``home_channel`` key in the returned dict is handled by
the core hook it becomes a proper ``HomeChannel`` dataclass on the
``PlatformConfig`` rather than being merged into ``extra``.
"""
server = os.getenv("IRC_SERVER", "").strip()
channel = os.getenv("IRC_CHANNEL", "").strip()
if not (server and channel):
return None
seed: dict = {
"server": server,
"channel": channel,
}
port = os.getenv("IRC_PORT", "").strip()
if port:
try:
seed["port"] = int(port)
except ValueError:
pass
nickname = os.getenv("IRC_NICKNAME", "").strip()
if nickname:
seed["nickname"] = nickname
use_tls = os.getenv("IRC_USE_TLS", "").strip().lower()
if use_tls:
seed["use_tls"] = use_tls in ("1", "true", "yes")
# Passwords live in PlatformConfig.extra as well for back-compat with
# existing config.yaml users; env-reads at construct time still win.
if os.getenv("IRC_SERVER_PASSWORD"):
seed["server_password"] = os.getenv("IRC_SERVER_PASSWORD")
if os.getenv("IRC_NICKSERV_PASSWORD"):
seed["nickserv_password"] = os.getenv("IRC_NICKSERV_PASSWORD")
# Optional home-channel (usually the same as IRC_CHANNEL, but can be a
# dedicated reports channel). Defaults to IRC_CHANNEL so cron jobs
# with ``deliver=irc`` have a sensible target without extra config.
home = os.getenv("IRC_HOME_CHANNEL") or channel
if home:
seed["home_channel"] = {
"chat_id": home,
"name": os.getenv("IRC_HOME_CHANNEL_NAME", home),
}
return seed
def register(ctx): def register(ctx):
"""Plugin entry point — called by the Hermes plugin system.""" """Plugin entry point — called by the Hermes plugin system."""
ctx.register_platform( ctx.register_platform(
@ -665,6 +716,14 @@ def register(ctx):
required_env=["IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"], required_env=["IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"],
install_hint="No extra packages needed (stdlib only)", install_hint="No extra packages needed (stdlib only)",
setup_fn=interactive_setup, setup_fn=interactive_setup,
# Env-driven auto-configuration — seeds PlatformConfig.extra with
# server/channel/port/tls + home_channel so env-only setups show
# up in gateway status without instantiating the adapter.
env_enablement_fn=_env_enablement,
# Cron home-channel delivery support. IRC_HOME_CHANNEL defaults to
# IRC_CHANNEL (see _env_enablement), so cron jobs with
# deliver=irc route to the joined channel by default.
cron_deliver_env_var="IRC_HOME_CHANNEL",
# Auth env vars for _is_user_authorized() integration # Auth env vars for _is_user_authorized() integration
allowed_users_env="IRC_ALLOWED_USERS", allowed_users_env="IRC_ALLOWED_USERS",
allow_all_env="IRC_ALLOW_ALL_USERS", allow_all_env="IRC_ALLOW_ALL_USERS",

View file

@ -1,4 +1,5 @@
name: irc-platform name: irc-platform
label: IRC
kind: platform kind: platform
version: 1.0.0 version: 1.0.0
description: > description: >
@ -7,7 +8,47 @@ description: >
(or DMs) and the Hermes agent. No external dependencies — uses (or DMs) and the Hermes agent. No external dependencies — uses
Python's stdlib asyncio for the IRC protocol. Python's stdlib asyncio for the IRC protocol.
author: Nous Research author: Nous Research
# ``requires_env`` entries are surfaced in ``hermes config`` UI via the
# platform-plugin env var injector in ``hermes_cli/config.py``.
requires_env: requires_env:
- IRC_SERVER - name: IRC_SERVER
- IRC_CHANNEL description: "IRC server hostname (e.g. irc.libera.chat)"
- IRC_NICKNAME prompt: "IRC server"
password: false
- name: IRC_CHANNEL
description: "Channel to join (e.g. #hermes — comma-separate for multiple)"
prompt: "IRC channel"
password: false
- name: IRC_NICKNAME
description: "Bot nickname on IRC (default: hermes-bot)"
prompt: "Bot nickname"
password: false
optional_env:
- name: IRC_PORT
description: "IRC server port (default: 6697 with TLS, 6667 without)"
prompt: "IRC port"
password: false
- name: IRC_USE_TLS
description: "Use TLS for the IRC connection (1/true/yes to enable, default: true on port 6697)"
prompt: "Use TLS? (true/false)"
password: false
- name: IRC_SERVER_PASSWORD
description: "Server password for the IRC PASS command (optional)"
prompt: "Server password (optional)"
password: true
- name: IRC_NICKSERV_PASSWORD
description: "NickServ password for automatic IDENTIFY on connect (optional)"
prompt: "NickServ password (optional)"
password: true
- name: IRC_ALLOWED_USERS
description: "Comma-separated IRC nicks allowed to talk to the bot"
prompt: "Allowed nicks (comma-separated)"
password: false
- name: IRC_ALLOW_ALL_USERS
description: "Allow anyone in the channel to talk to the bot (dev only)"
prompt: "Allow all users? (true/false)"
password: false
- name: IRC_HOME_CHANNEL
description: "Channel for cron / notification delivery (defaults to IRC_CHANNEL)"
prompt: "Home channel (or empty)"
password: false

View file

@ -152,6 +152,42 @@ def is_connected(config) -> bool:
return validate_config(config) return validate_config(config)
def _env_enablement() -> dict | None:
"""Seed ``PlatformConfig.extra`` from env vars during gateway config load.
Called by the platform registry's env-enablement hook BEFORE adapter
construction, so ``gateway status`` and ``get_connected_platforms()``
reflect env-only configuration without instantiating the Teams SDK.
Returns ``None`` when Teams isn't minimally configured.
The special ``home_channel`` key in the returned dict becomes a proper
``HomeChannel`` dataclass on the ``PlatformConfig`` via the core hook.
"""
client_id = os.getenv("TEAMS_CLIENT_ID", "").strip()
client_secret = os.getenv("TEAMS_CLIENT_SECRET", "").strip()
tenant_id = os.getenv("TEAMS_TENANT_ID", "").strip()
if not (client_id and client_secret and tenant_id):
return None
seed: dict = {
"client_id": client_id,
"client_secret": client_secret,
"tenant_id": tenant_id,
}
port = os.getenv("TEAMS_PORT", "").strip()
if port:
try:
seed["port"] = int(port)
except ValueError:
pass
home = os.getenv("TEAMS_HOME_CHANNEL", "").strip()
if home:
seed["home_channel"] = {
"chat_id": home,
"name": os.getenv("TEAMS_HOME_CHANNEL_NAME", "Home"),
}
return seed
# Keep the old name as an alias so existing test imports don't break. # Keep the old name as an alias so existing test imports don't break.
check_teams_requirements = check_requirements check_teams_requirements = check_requirements
@ -702,6 +738,14 @@ def register(ctx) -> None:
required_env=["TEAMS_CLIENT_ID", "TEAMS_CLIENT_SECRET", "TEAMS_TENANT_ID"], required_env=["TEAMS_CLIENT_ID", "TEAMS_CLIENT_SECRET", "TEAMS_TENANT_ID"],
install_hint="pip install microsoft-teams-apps aiohttp", install_hint="pip install microsoft-teams-apps aiohttp",
setup_fn=interactive_setup, setup_fn=interactive_setup,
# Env-driven auto-configuration — seeds PlatformConfig.extra with
# client_id/secret/tenant + port + home_channel so env-only setups
# show up in gateway status without instantiating the Teams SDK.
env_enablement_fn=_env_enablement,
# Cron home-channel delivery support. Lets deliver=teams cron
# jobs route to the configured Teams chat/channel without editing
# cron/scheduler.py's hardcoded sets.
cron_deliver_env_var="TEAMS_HOME_CHANNEL",
# Auth env vars for _is_user_authorized() integration # Auth env vars for _is_user_authorized() integration
allowed_users_env="TEAMS_ALLOWED_USERS", allowed_users_env="TEAMS_ALLOWED_USERS",
allow_all_env="TEAMS_ALLOW_ALL_USERS", allow_all_env="TEAMS_ALLOW_ALL_USERS",

View file

@ -1,4 +1,5 @@
name: teams-platform name: teams-platform
label: Microsoft Teams
kind: platform kind: platform
version: 1.0.0 version: 1.0.0
description: > description: >
@ -7,7 +8,41 @@ description: >
between Teams chats (personal DMs, group chats, channel posts) and between Teams chats (personal DMs, group chats, channel posts) and
the Hermes agent. Supports Adaptive Card approval prompts. the Hermes agent. Supports Adaptive Card approval prompts.
author: Aamir Jawaid author: Aamir Jawaid
# ``requires_env`` entries are surfaced in ``hermes config`` UI via the
# platform-plugin env var injector in ``hermes_cli/config.py``.
requires_env: requires_env:
- TEAMS_CLIENT_ID - name: TEAMS_CLIENT_ID
- TEAMS_CLIENT_SECRET description: "Azure AD application (Bot Framework) client ID"
- TEAMS_TENANT_ID prompt: "Teams / Azure AD client ID"
url: "https://portal.azure.com/"
password: false
- name: TEAMS_CLIENT_SECRET
description: "Azure AD application client secret"
prompt: "Teams / Azure AD client secret"
url: "https://portal.azure.com/"
password: true
- name: TEAMS_TENANT_ID
description: "Azure AD tenant ID hosting the bot application"
prompt: "Teams / Azure AD tenant ID"
password: false
optional_env:
- name: TEAMS_PORT
description: "Webhook listen port (Bot Framework default: 3978)"
prompt: "Webhook port"
password: false
- name: TEAMS_ALLOWED_USERS
description: "Comma-separated Teams user IDs / UPNs allowed to talk to the bot"
prompt: "Allowed users (comma-separated)"
password: false
- name: TEAMS_ALLOW_ALL_USERS
description: "Allow any Teams user to trigger the bot (dev only)"
prompt: "Allow all users? (true/false)"
password: false
- name: TEAMS_HOME_CHANNEL
description: "Default chat/channel ID for cron / notification delivery"
prompt: "Home channel (or empty)"
password: false
- name: TEAMS_HOME_CHANNEL_NAME
description: "Display name for the Teams home channel"
prompt: "Home channel display name"
password: false