feat(plugins): add standalone_sender_fn for out-of-process cron delivery - #21805
Closed
GodsBoy wants to merge 1 commit into
Closed
feat(plugins): add standalone_sender_fn for out-of-process cron delivery#21805GodsBoy wants to merge 1 commit into
GodsBoy wants to merge 1 commit into
Conversation
Plugin platforms (IRC, Teams, Google Chat) currently fail with `No live adapter for platform '<name>'` when a `deliver=<plugin>` cron job runs in a separate process from the gateway, even though the platforms are eligible cron targets via `cron_deliver_env_var` (added in NousResearch#21306). Built-in platforms (Telegram, Discord, Slack, etc.) use direct REST helpers in `tools/send_message_tool.py` so cron can deliver without holding the gateway in the same process; plugin platforms historically depended on `_gateway_runner_ref()` which returns `None` out of process. This change adds an optional `standalone_sender_fn` field to `PlatformEntry` so plugins can register an ephemeral send path that opens its own connection, sends, and closes without needing the live adapter. The dispatch site in `_send_via_adapter` falls through to the hook when the gateway runner is unavailable, with a descriptive error when neither path applies. The hook is optional, so existing plugins are unaffected. Reference migrations land in the same change for IRC, Teams, and Google Chat, exercising the hook across stdlib (asyncio + IRC protocol), Bot Framework OAuth client_credentials, and Google service-account flows respectively. Security hardening on the new code paths: * IRC: control-character stripping on chat_id and message body to block CRLF command injection; bounded nick-collision retries; JOIN before PRIVMSG so channels with the default `+n` mode accept the delivery. * Teams: TEAMS_SERVICE_URL validated against an allowlist of known Bot Framework hosts (`smba.trafficmanager.net`, `smba.infra.gov.teams.microsoft.us`) to block SSRF; chat_id and tenant_id constrained to the documented Bot Framework character set; per-request timeouts so a slow STS endpoint cannot starve the activity POST. * Google Chat: chat_id and thread_id validated against strict resource-name regexes; service-account refresh wrapped in `asyncio.wait_for` so a hung token endpoint cannot stall the scheduler. Test coverage: 20 new tests covering happy path, missing-config errors, network failure modes, and each defensive validation. Existing tests unchanged. `bash scripts/run_tests.sh tests/tools/test_send_message_tool.py tests/gateway/test_irc_adapter.py tests/gateway/test_teams.py tests/gateway/test_google_chat.py` reports 341 passed, 0 regressions. Documentation: new "Out-of-process cron delivery" section in website/docs/developer-guide/adding-platform-adapters.md and an entry in gateway/platforms/ADDING_A_PLATFORM.md naming the hook.
Contributor
Author
CI Status SummaryAll CI failures on this PR are unrelated to the changes. Posting this for triage transparency. Failures and their root causes
Local test verification
Happy to trim scope, split, or add additional tests if any of the above reads differently to a maintainer. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds an optional
standalone_sender_fnfield toPlatformEntryso plugin platforms (IRC, Teams, Google Chat) can register an out-of-process send path. Without this,deliver=<plugin>cron jobs fail withNo live adapter for platform '<name>'when cron runs in a separate process from the gateway, even thoughcron_deliver_env_var(added in #21306) declared those platforms as eligible cron targets.This is the missing third phase of plugin platform parity:
2e20f6ae2(Apr 11) added in-process_send_via_adapter.af9336d57(May 7) addedcron_deliver_env_varso plugins become eligible cron targets.The hook is optional; existing plugins are unaffected.
Related Issue
Fixes #21804
Type of Change
Changes Made
gateway/platform_registry.py: new optionalstandalone_sender_fn: Optional[Callable[..., Awaitable[dict]]]field onPlatformEntry.tools/send_message_tool.py:_send_via_adapternow falls through to the hook when_gateway_runner_ref()isNone. Forwardsthread_id,media_files,force_documentkwargs. Validates the return shape. Re-raisesasyncio.CancelledErrorinstead of swallowing it. Restores the helpfulIs the gateway running with this platform connected?suffix in the fall-through error and adds guidance on the new hook.plugins/platforms/irc/adapter.py: stdlib-only_standalone_sendthat opens an ephemeral asyncio TCP/TLS connection with a-cronnick suffix (avoiding NICK collisions with the live gateway adapter), JOINs the channel before PRIVMSG so the default+nchannel mode accepts the delivery, and QUITs cleanly. Also removes the dead_ensure_importsno-op.plugins/platforms/teams/adapter.py:_standalone_sendperforms an OAuthclient_credentialstoken grant againstlogin.microsoftonline.com, then POSTs the activity to the Bot Framework/v3/conversations/<id>/activitiesendpoint.TEAMS_SERVICE_URLis validated against an allowlist of known Bot Framework hosts.plugins/platforms/google_chat/adapter.py:_standalone_sendresolves service-account credentials (inline JSON, file path, or ADC), refreshes the token under anasyncio.wait_fortimeout, and POSTs to the Chat REST API.chat_idandthread_idare validated against strict resource-name regexes.tests/tools/test_send_message_tool.py: 5 dispatch tests.tests/gateway/test_irc_adapter.py: 6 IRC tests including JOIN ordering and CRLF-injection guards.tests/gateway/test_teams.py: 5 Teams tests including SSRF allowlist and chat_id path-traversal guards.tests/gateway/test_google_chat.py: 4 Google Chat tests including chat_id path-traversal guard.website/docs/developer-guide/adding-platform-adapters.md: new section under Cron Delivery covering the hook signature, return contract, and exception handling.gateway/platforms/ADDING_A_PLATFORM.md: hook listed in the optional-hooks summary.How to Test
Reproducing the original bug (before this PR)
IRC_SERVER,IRC_NICKNAME,IRC_CHANNEL,IRC_HOME_CHANNELin~/.hermes/.env.hermes gateway.hermes cron run.hermes cron add --deliver=irc --schedule="*/2 * * * *" --prompt="say hello".Job '...': delivery error: No live adapter for platform 'irc'....Verifying the fix
Job '...': delivered to irc:#<channel>. The message arrives in the configured channel.Automated tests
bash scripts/run_tests.sh tests/tools/test_send_message_tool.py tests/gateway/test_irc_adapter.py tests/gateway/test_teams.py tests/gateway/test_google_chat.pyreports 341 passed, 0 regressions, 7 pre-existing warnings.Security
The new code paths receive operator-controlled config (env vars,
pconfig.extra) and reach external services with bearer tokens. Defenses included:chat_idor message body_strip_irc_control_charsblanks\r/\n/\x00;chat_idrejected outright if it contains line terminators or whitespaceTEAMS_SERVICE_URLsmba.trafficmanager.net,smba.infra.gov.teams.microsoft.us); HTTPS-onlychat_idcreds.refreshwrapped inasyncio.wait_for(timeout=10)asyncio.wait_for(timeout=5)except ExceptionswallowingCancelledErrorCancelledErrorTests cover each guard.
Checklist
Code
feat(plugins): ...)hermes sendto pipe script output to any messaging platform #19631 CLIhermes send, neither overlaps the out-of-process plugin dispatch site)pytest tests/on the touched files and all tests pass (341 passed)Documentation & Housekeeping
adding-platform-adapters.md,ADDING_A_PLATFORM.md)cli-config.yaml.exampleif I added/changed config keys: N/A (no new config keys, only platform-plugin-internal env vars)CONTRIBUTING.mdorAGENTS.md: N/A (no architectural change to user-facing workflows)asyncio+aiohttp; no POSIX-only calls.send_messagetool's externally observable contract is unchanged when no plugin registers the hook; the only change is the cron error string now mentions the new option in addition to the existing "is the gateway running" guidance.Notes for reviewers
gateway/platform_registry.py:128-148; the signature mirrors the internal_send_via_adaptershape so review friction is minimal.-cronnick suffix to avoid NICK collisions with a live gateway adapter holding the configured nickname on the same network.media_filesandforce_documentkwargs are forwarded to the hook for signature parity, but the three reference implementations send text-only (with a docstring note). The live adapter still handles attachments via the SDK; cron jobs typically deliver text summaries.learnings-researcherstep in pre-PR review noted that this work is the natural completion of the in-flight Phase 2 / Phase 3 plugin parity effort. The dispatch shape, registry field convention, and per-plugin migration shape all match the patterns Teknium established in2e20f6ae2andaf9336d57.