feat(ntfy): add ntfy platform adapter as a plugin (salvages #30625)#30867
Conversation
… fix, and 81 tests
ntfy now ships as a self-contained plugin under plugins/platforms/ntfy/ instead of editing 8 core files (gateway/config.py Platform enum, gateway/run.py factory + auth maps, cron/scheduler.py, toolsets.py, hermes_cli/status.py, agent/prompt_builder.py, gateway/channel_directory.py, tools/send_message_tool.py). All routing goes through gateway/platform_registry via register_platform(): - adapter_factory, check_fn, validate_config, is_connected - env_enablement_fn seeds PlatformConfig.extra from NTFY_* env vars so gateway status reflects env-only setups without instantiating httpx - standalone_sender_fn handles deliver=ntfy cron jobs when cron runs out-of-process from the gateway - allowed_users_env / allow_all_env hook into _is_user_authorized - cron_deliver_env_var=NTFY_HOME_CHANNEL for cron home routing - platform_hint surfaces in the system prompt - pii_safe=True (topic names are the only identifier; no PII to redact) Tests moved to tests/gateway/test_ntfy_plugin.py using _plugin_adapter_loader so the module lives under plugin_adapter_ntfy in sys.modules and cannot collide with sibling plugin-adapter tests on the same xdist worker. The core-file grep tests (Platform.NTFY in source, hermes-ntfy in toolsets, etc.) are replaced with plugin-shape tests covering register() metadata, env_enablement_fn output, and standalone_sender_fn behavior. 68 tests pass under scripts/run_tests.sh.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
4 |
unresolved-import |
2 |
First entries
plugins/platforms/ntfy/adapter.py:246: [unresolved-attribute] unresolved-attribute: Attribute `Timeout` is not defined on `None` in union `Unknown | None`
plugins/platforms/ntfy/adapter.py:241: [unresolved-attribute] unresolved-attribute: Attribute `stream` is not defined on `None` in union `Unknown | None`
plugins/platforms/ntfy/adapter.py:57: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
plugins/platforms/ntfy/adapter.py:530: [unresolved-attribute] unresolved-attribute: Attribute `AsyncClient` is not defined on `None` in union `Unknown | None`
plugins/platforms/ntfy/adapter.py:415: [unresolved-attribute] unresolved-attribute: Attribute `TimeoutException` is not defined on `None` in union `Unknown | None`
tests/gateway/test_ntfy_plugin.py:21: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 4805 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
mohamedorigami-jpg
left a comment
There was a problem hiding this comment.
Solid contribution. The ntfy adapter covers the full surface: config (env vars + YAML), streaming receive via httpx, structured delivery, and integration into the platform enum/scheduler/channel directory.
A few things that stood out positively:
- Env var bridge in config.py is thorough -- NTFY_TOPIC, NTFY_SERVER_URL, NTFY_TOKEN, NTFY_PUBLISH_TOPIC, NTFY_HOME_CHANNEL, NTFY_MARKDOWN all covered. Matches the pattern used by other platform adapters.
- Prompt builder integration with clear ntfy-specific guidance (plain text default, optional markdown, push notification context) -- this matters for agent quality-of-life on constrained channels.
- Clean salvage from #30625 -- the commit history/description should help reviewers trace the original context.
- Skip session discovery in channel_directory.py is the right call for a push-only channel.
One question: have you tested this against a self-hosted ntfy instance or just ntfy.sh? The httpx streaming pattern should work with both, but auth token handling can differ (Basic vs Bearer depending on server config). Might be worth a note in the PR description or a follow-up doc.
Robustness: - Surface 401/404 stream failures via _set_fatal_error() so the gateway's runtime status reflects 'fatal: ntfy_unauthorized' / 'ntfy_topic_not_found' instead of staying 'connected' when the reconnect loop halts. Matches the pattern in whatsapp / telegram / sms adapters. - Strip whitespace from auth tokens so pasted tokens with trailing newlines don't produce malformed Authorization headers. Simplicity: - Extract _build_auth_header() and _truncate_body() to module-level helpers, used by both NtfyAdapter and _standalone_send. Removes the duplicated auth/truncation logic between the two paths. Docs: - website/docs/user-guide/messaging/ntfy.md — full setup guide, identity-model warning, self-hosting, cron usage, troubleshooting. - website/docs/reference/environment-variables.md — all 9 NTFY_* vars. - website/docs/user-guide/messaging/index.md — platform comparison row. - website/sidebars.ts — sidebar entry between simplex and open-webui. Tests: 78/78 (+ 10 new robustness tests covering token hygiene, fatal error propagation for 401/404, and the _truncate_body helper).
|
Pushed a few more bits on top of what you already did:
All 80 tests pass. Let me know if there's anything else needed on this one. |
Summary
ntfy push-notification adapter, shipped as a platform plugin under
plugins/platforms/ntfy/— zero edits to core gateway/cron/toolsets/CLI files.Salvages #30625 (@sprmn24, originally #4043). The contributor's adapter code, tests, and security/UX feedback addressed in the original reopen are preserved via cherry-pick. The follow-up commit reshapes it from a built-in adapter into a plugin so future ntfy work doesn't have to touch core.
Why a plugin
The original PR added
Platform.NTFYto the enum and edited 8 core files (gateway/config.py,gateway/run.py,cron/scheduler.py,toolsets.py,hermes_cli/status.py,agent/prompt_builder.py,gateway/channel_directory.py,tools/send_message_tool.py). New platforms should go throughgateway/platform_registrylike IRC, SimpleX, Teams, LINE, Discord, etc. — register-time wiring, no core diff.Changes
plugins/platforms/ntfy/adapter.py—NtfyAdapter+register(ctx)callingctx.register_platform(...). All gateway integration goes through the registry entry:env_enablement_fnseedsPlatformConfig.extrafromNTFY_*env vars sogateway statusreflects env-only setups without instantiating httpxstandalone_sender_fnhandlesdeliver=ntfycron jobs when cron runs out-of-process from the gatewayallowed_users_env=NTFY_ALLOWED_USERS/allow_all_env=NTFY_ALLOW_ALL_USERShook into_is_user_authorizedcron_deliver_env_var=NTFY_HOME_CHANNELfor cron home routingplatform_hintsurfaces in the system promptpii_safe=True(topic names are the only identifier)plugins/platforms/ntfy/plugin.yaml— manifest withrequires_env/optional_envforhermes configUItests/gateway/test_ntfy_plugin.py— 68 tests, loaded via_plugin_adapter_loader(collision-safe under xdist). Replaces the core-file grep tests from the original PR with plugin-shape tests coveringregister()metadata,_env_enablementoutputs, and_standalone_sendbehavior.Reviewer feedback from #4043 (preserved from @sprmn24's reopen)
user_idis the topic name, never the publisher-controlledtitle.test_unknown_publisher_cannot_impersonate_allowed_userenforces this._FatalStreamErrorhalts the reconnect loop on auth/topic failures instead of hammering every 60s.logger.warning()when content > 4096 chars.check_requirements()readsNTFY_TOPICdirectly, noload_gateway_config()per pre-flight.markdown: truein extra (orNTFY_MARKDOWN=true) sendsX-Markdown: trueheader.Validation
tests/gateway/test_ntfy_plugin.pytests/gateway/test_run.py+test_config.py+tests/cron/+tests/test_toolsets.pyPlatform("ntfy")resolves via plugin filesystem scanNTFY_TOPIC=fooenv-only setup →cfg.platforms[Platform.NTFY]populated, home_channel wiredplatform_registry.get("ntfy")returns entry with all hooks attachedCloses #30625. Credit @sprmn24 — adapter design, security fixes, and original test coverage all theirs.
Infographic