diff --git a/plugins/platforms/irc/adapter.py b/plugins/platforms/irc/adapter.py index a9eea62ba2..c328434435 100644 --- a/plugins/platforms/irc/adapter.py +++ b/plugins/platforms/irc/adapter.py @@ -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", diff --git a/plugins/platforms/irc/plugin.yaml b/plugins/platforms/irc/plugin.yaml index 1e3d19f48c..ccf83c4a03 100644 --- a/plugins/platforms/irc/plugin.yaml +++ b/plugins/platforms/irc/plugin.yaml @@ -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 diff --git a/plugins/platforms/teams/adapter.py b/plugins/platforms/teams/adapter.py index f30627ace6..7e17a7c2be 100644 --- a/plugins/platforms/teams/adapter.py +++ b/plugins/platforms/teams/adapter.py @@ -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", diff --git a/plugins/platforms/teams/plugin.yaml b/plugins/platforms/teams/plugin.yaml index 57f18adaa1..fd23756035 100644 --- a/plugins/platforms/teams/plugin.yaml +++ b/plugins/platforms/teams/plugin.yaml @@ -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