Skip to content

feat(notifications): ntfy/Slack/Discord/generic webhook for the push events - #523

Open
opticon454 wants to merge 2 commits into
Ark0N:masterfrom
opticon454:feat/webhook-notifications
Open

opticon454 wants to merge 2 commits into
Ark0N:masterfrom
opticon454:feat/webhook-notifications

Conversation

@opticon454

Copy link
Copy Markdown
Contributor

What

Opt-in webhook notifications. The same events that trigger Web Push (permission prompts, questions, errors, respawn blocked, crash loop, task complete, idle, response complete) can also be posted to ntfy, Slack, Discord, or any JSON URL, so a headless server can reach a phone with no browser tab open and no push subscription.

Settings → Notifications → "Webhook (ntfy, Slack, Discord)": enable, pick the service, paste the URL, choose "Needs attention" (skips the routine "response complete") or "Everything", Save, Send test. Off by default.

Design decisions worth a look

  • The URL is a bearer secret (anyone holding a Slack/Discord URL can post as it), and GET /api/settings is readable by every logged-in user. So it is not a setting: it lives in ~/.codeman/webhook.json (0600, tmp+rename, like custom-model-hosts.json), is only written through PUT /api/webhook, and is never returned (the API and UI show scheme + host only). The URL box is write-only; changing only the service or scope never needs the secret re-sent. All three routes are admin-only in multi-user mode (the channel receives every session's events, the same reach an admin's own Web Push has).
  • Delivery reuses the web-tab egress guard (webviewFetch): link-local and cloud-metadata targets are refused on the resolved address at connect time; save-time validation also rejects non-http(s), user:pass@, and those targets. Loopback and LAN stay allowed on purpose (a local ntfy is the feature). Redirects are not followed, the call times out after 5 s, and error text never contains the URL.
  • Agent text can't ping a channel: Discord gets allowed_mentions: { parse: [] }, Slack control characters (<!channel>, <@U…>, <url|text>) are escaped, and ntfy headers are RFC 2047-encoded so a title can't inject a header.
  • Fire-and-forget: the send runs before the "no push subscriptions" early return in sendPushNotifications (that is the headless case), never delays Web Push, dedupes the same event+session within 3 s, and caps in-flight requests at 5. The notification body text moved into a small static method so both channels share it; Web Push output is unchanged.

Tests

  • test/webhook-notify.test.ts (39): URL validation, masking, scope, each service's request format, store (0600, tightens an existing 0644 file, coerces bad values), sender (status/redirect/timeout/network errors with no URL leak), notifier (dedupe, in-flight cap, test send), plus real delivery to a local HTTP server through webviewFetch and refusal of a metadata address / redirect-to-metadata.
  • test/routes/webhook-routes.test.ts (17): masking, partial updates, clearing, 400s, strict schema, test route, multi-user gating.
  • test/webhook-settings.browser.test.ts (6): real server + real Chromium + a local receiver: the UI saves, masks, sends a test that arrives with the ntfy headers, and shows a failing endpoint without exposing the URL.
  • Full CI gate: typecheck, lint, format, public assets, catalogue and 8564 tests pass (two git-clone tests timed out once under load and pass alone, unrelated to this change).

🤖 Generated with Claude Code

opticon454 and others added 2 commits October 2, 2026 22:06
…events

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rowser test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@opticon454

Copy link
Copy Markdown
Contributor Author

Heads-up on merge order. I checked my open PRs against each other with trial merges. Every overlap is a textual conflict in a shared registry or list (adjacent insertions), not a behavioural interaction, but whichever lands first, the others need a rebase:

I'm not changing this PR for it. As the others merge I will rebase this one onto master, resolve the conflicts, re-run the full gate and push. If you have a preferred merge order, tell me and I will follow it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant