Skip to content

feat(ntfy): add ntfy platform adapter as a plugin (salvages #30625)#30867

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-1a67f7a8
May 23, 2026
Merged

feat(ntfy): add ntfy platform adapter as a plugin (salvages #30625)#30867
teknium1 merged 3 commits into
mainfrom
hermes/hermes-1a67f7a8

Conversation

@teknium1

@teknium1 teknium1 commented May 23, 2026

Copy link
Copy Markdown
Contributor

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.NTFY to 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 through gateway/platform_registry like IRC, SimpleX, Teams, LINE, Discord, etc. — register-time wiring, no core diff.

Changes

  • plugins/platforms/ntfy/adapter.pyNtfyAdapter + register(ctx) calling ctx.register_platform(...). All gateway integration goes through the registry entry:
    • 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=NTFY_ALLOWED_USERS / allow_all_env=NTFY_ALLOW_ALL_USERS 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)
  • plugins/platforms/ntfy/plugin.yaml — manifest with requires_env/optional_env for hermes config UI
  • tests/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 covering register() metadata, _env_enablement outputs, and _standalone_send behavior.

Reviewer feedback from #4043 (preserved from @sprmn24's reopen)

  • Identity spoofing (@TheophileDiot): user_id is the topic name, never the publisher-controlled title. test_unknown_publisher_cannot_impersonate_allowed_user enforces this.
  • 401/404 fatal stop: _FatalStreamError halts the reconnect loop on auth/topic failures instead of hammering every 60s.
  • Backoff reset: index resets to 0 if a stream stays alive ≥60s.
  • Truncation visibility: logger.warning() when content > 4096 chars.
  • Light pre-check: check_requirements() reads NTFY_TOPIC directly, no load_gateway_config() per pre-flight.
  • Configurable markdown (@gerrydoro): markdown: true in extra (or NTFY_MARKDOWN=true) sends X-Markdown: true header.

Validation

Result
tests/gateway/test_ntfy_plugin.py 68/68 pass
tests/gateway/test_run.py + test_config.py + tests/cron/ + tests/test_toolsets.py 448/448 pass
E2E: Platform("ntfy") resolves via plugin filesystem scan
E2E: NTFY_TOPIC=foo env-only setup → cfg.platforms[Platform.NTFY] populated, home_channel wired
E2E: platform_registry.get("ntfy") returns entry with all hooks attached

Closes #30625. Credit @sprmn24 — adapter design, security fixes, and original test coverage all theirs.

Infographic

ntfy-plugin-migration

sprmn24 and others added 2 commits May 23, 2026 02:31
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.
@github-actions

github-actions Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-1a67f7a8 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9037 on HEAD, 9029 on base (🆕 +8)

🆕 New issues (6):

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.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins platform/webhook Webhook / API server labels May 23, 2026

@mohamedorigami-jpg mohamedorigami-jpg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
@mohamedorigami-jpg

Copy link
Copy Markdown
Contributor

Pushed a few more bits on top of what you already did:

  • adapter.send() now uses the shared _truncate_body helper instead of inline truncation (keeps truncation logic in one place)
  • Added 403 handling to _consume_stream with fatal error propagation (ntfy_forbidden) - same pattern as the 401/404 handling you already added
  • Added whitespace stripping for NTFY_SERVER_URL in _env_enablement for consistency with the other env vars

All 80 tests pass. Let me know if there's anything else needed on this one.

@teknium1 teknium1 merged commit 3b096d6 into main May 23, 2026
27 checks passed
@teknium1 teknium1 deleted the hermes/hermes-1a67f7a8 branch May 23, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/webhook Webhook / API server type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants