Outbound webhook notifications for proposal creation - #331
Conversation
Adds AGENT_VAULT_NOTIFY_WEBHOOK_URL / AGENT_VAULT_NOTIFY_FORMAT so teams
that don't watch email can route proposal.created events to Slack,
Discord, or a generic HTTP endpoint, following the same "env var enables
it" pattern as AGENT_VAULT_SMTP_*.
- internal/notify: new WebhookConfig + Notifier.SendProposalWebhook,
generic JSON and Slack-compatible {"text": ...} payload shapes
- server: fire webhook independently of email on proposal creation (a
vault with no human members still gets notified), plus an owner-only
/v1/admin/notify/webhook/test endpoint and settings flag
- cli: `agent-vault owner notify webhook test`
- web: matching "Test Webhook" section in instance settings
- docs: .env.example, environment-variables.mdx, reference/cli.mdx,
learn/proposals.mdx
Closes Infisical#328
|
✅ CLA satisfied. All contributors have signed the current CLA. The |
|
| Filename | Overview |
|---|---|
| internal/notify/webhook.go | Adds webhook configuration and delivery, but the HTTP client omits outbound destination controls. |
| internal/server/handle_proposals.go | Separates email and webhook fan-out, but byte-based truncation can corrupt Unicode messages. |
| internal/server/handle_settings.go | Adds the owner-checked webhook test handler and exposes webhook configuration state. |
| internal/server/server.go | Registers the authenticated webhook test route. |
| cmd/notify.go | Adds the nested owner command for sending a test webhook. |
| web/src/pages/instance/SettingsTab.tsx | Adds webhook configuration status and test controls to instance settings. |
| docs/self-hosting/environment-variables.mdx | Documents webhook configuration, but the test command omits its owner parent. |
Reviews (1): Last reviewed commit: "feat: outbound webhook notifications for..." | Re-trigger Greptile
|
|
||
| // webhookHTTPClient is shared across sends; short timeout since this is a | ||
| // fire-and-forget best-effort notification, not on the request's critical path. | ||
| var webhookHTTPClient = &http.Client{Timeout: 10 * time.Second} |
There was a problem hiding this comment.
Webhook Bypasses Network Policy
This client uses the default transport and follows redirects without applying the existing private-range and allowlist checks. A configured endpoint that resolves or redirects to loopback, link-local, cloud metadata, or another internal address can make proposal creation send requests into the server's private network.
Context Used: Flag SSRF risks (source)
| // Truncate for notification bodies. | ||
| msg := message |
There was a problem hiding this comment.
Byte Truncation Corrupts Unicode
When byte 200 falls inside a multibyte character, msg[:200] produces invalid UTF-8. JSON encoding replaces the broken character and the email path receives the same malformed string, so proposal messages containing emoji or other non-ASCII text can be corrupted in both notification channels.
| ``` | ||
|
|
||
| <Tip> | ||
| To verify the webhook is working, run `agent-vault notify webhook test`. It sends a synthetic `proposal.created` event to the configured URL. |
There was a problem hiding this comment.
Documented Command Misses Parent
The new command is registered below owner, so the documented agent-vault notify webhook test invocation fails as an unknown command. The working command is agent-vault owner notify webhook test, as shown in the CLI reference.
| To verify the webhook is working, run `agent-vault notify webhook test`. It sends a synthetic `proposal.created` event to the configured URL. | |
| To verify the webhook is working, run `agent-vault owner notify webhook test`. It sends a synthetic `proposal.created` event to the configured URL. |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
- Apply netguard SSRF protection to the outbound webhook HTTP client so an operator-supplied webhook URL (or a redirect it returns) can't be used to reach loopback, RFC-1918, link-local, or cloud metadata addresses. - Truncate proposal notification messages by rune count instead of byte count, so multi-byte UTF-8 characters (emoji, accents, CJK) aren't split and corrupted when a message exceeds 200 characters. - Fix the documented webhook test command to include the `owner` parent command, matching the actual CLI surface.
|
Thanks for the review, @greptile-apps! Addressed all three points in ecea411:
All existing + new tests pass ( |
|
All three fixes look correct and well-implemented. Here's a quick summary of what's in ecea411:
The LGTM. Thanks for the thorough follow-up. Tip: You can customize Greptile's behavior for this repo with |
|
All Greptile review comments have been addressed and fixed (see commit ecea411 above) — SSRF protection added to the webhook client, Unicode-safe truncation, and the docs typo fixed. This should be ready for a maintainer review whenever you get a chance. Would appreciate it if someone from the team could take a look and merge if it looks good. Thanks! |
Closes #328.
What
Adds outbound webhook notifications for proposal lifecycle events, following the same "env var enables it" pattern already used for SMTP (
AGENT_VAULT_SMTP_*):AGENT_VAULT_NOTIFY_WEBHOOK_URL— generic webhook, POSTs a JSON payload (proposal id, vault, agent, message, approval URL) onproposal.created.AGENT_VAULT_NOTIFY_FORMAT=generic|slack—slacksends a{"text": ...}body compatible with Slack Incoming Webhooks; close enough to Discord's webhook API to work there too without extra config.This is scoped as a notification side-channel only — it doesn't touch credential handling, the proxy path, or the approval/auth flow.
Why
Agent Vault's core UX loop depends on a human noticing and approving a proposal quickly. Email-only (and often not even configured) is a real gap for teams that live in Slack/Discord. The webhook channel fires independently of email — a vault with no human members (so no email recipients) still gets notified.
Changes
internal/notify: newWebhookConfig+Notifier.SendProposalWebhook, generic JSON and Slack-compatible payload shapes.internal/server: fire the webhook onproposal.createdindependently of the email path; new owner-onlyPOST /v1/admin/notify/webhook/testendpoint;webhook_configuredadded toGET /v1/admin/settings.cmd: newagent-vault owner notify webhook testCLI command, mirroringowner email test.web: matching "Test Webhook" section in the instance Settings tab..env.example,docs/self-hosting/environment-variables.mdx,docs/reference/cli.mdx,docs/learn/proposals.mdx.Testing
WebhookConfigloading,SendProposalWebhook(generic + Slack payloads, non-2xx handling, unreachable URL), the/v1/admin/notify/webhook/testhandler (owner-gating, not-configured, success, upstream failure), and a regression test proving the webhook fires even when a vault has zero email recipients.go build ./...,go vet ./..., andgo test ./...all pass (one pre-existing, unrelated failure ininternal/isolation— a Docker-socket-path test that also fails onmainin this sandbox, unrelated to this change).cd web && npx tsc --noEmitpasses.Follow-ups (happy to take these on separately if useful)
proposal.expiring_soon, 24h before the 7-day TTL) as a second nudge before a proposal expires.