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)
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):
"""Plugin entry point — called by the Hermes plugin system."""
ctx.register_platform(
@ -665,6 +716,14 @@ def register(ctx):
required_env=["IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"],
install_hint="No extra packages needed (stdlib only)",
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
allowed_users_env="IRC_ALLOWED_USERS",
allow_all_env="IRC_ALLOW_ALL_USERS",

View file

@ -1,4 +1,5 @@
name: irc-platform
label: IRC
kind: platform
version: 1.0.0
description: >
@ -7,7 +8,47 @@ description: >
(or DMs) and the Hermes agent. No external dependencies — uses
Python's stdlib asyncio for the IRC protocol.
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:
- IRC_SERVER
- IRC_CHANNEL
- IRC_NICKNAME
- name: IRC_SERVER
description: "IRC server hostname (e.g. irc.libera.chat)"
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)
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.
check_teams_requirements = check_requirements
@ -702,6 +738,14 @@ def register(ctx) -> None:
required_env=["TEAMS_CLIENT_ID", "TEAMS_CLIENT_SECRET", "TEAMS_TENANT_ID"],
install_hint="pip install microsoft-teams-apps aiohttp",
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
allowed_users_env="TEAMS_ALLOWED_USERS",
allow_all_env="TEAMS_ALLOW_ALL_USERS",

View file

@ -1,4 +1,5 @@
name: teams-platform
label: Microsoft Teams
kind: platform
version: 1.0.0
description: >
@ -7,7 +8,41 @@ description: >
between Teams chats (personal DMs, group chats, channel posts) and
the Hermes agent. Supports Adaptive Card approval prompts.
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:
- TEAMS_CLIENT_ID
- TEAMS_CLIENT_SECRET
- TEAMS_TENANT_ID
- name: TEAMS_CLIENT_ID
description: "Azure AD application (Bot Framework) client 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