feat(tools): add "webhook" toolset for outbound notifications#3641
feat(tools): add "webhook" toolset for outbound notifications#3641dwin-gharibi wants to merge 21 commits into
Conversation
… tool for docker agent
…to docker agents toolsets
…o docker agents toolset
…to solve linting problems
|
@Sayt-0 Problems solved. |
…iling-newline in webhook doc
Sayt-0
left a comment
There was a problem hiding this comment.
New webhook toolset fills a real gap (agents currently have no way to notify anyone) with a well-chosen scope: message-shaped payloads only, SSRF-safe client, pure payload builder with an injectable HTTP doer. Registration, catalog entry, docs layout and tests follow the existing patterns.
Two blocking items, both cheap to fix:
| # | Issue | Impact |
|---|---|---|
| 1 | Docs reference the scheduler toolset, which does not exist on main (#3632 is still open): overview prose, example config, closing tip |
Copy-pasting the example fails at load time with unknown toolset type: scheduler (pkg/teamloader/registry.go) |
| 2 | agent-schema.json toolset type enum not updated |
Editor validation (yaml-language-server, recommended in docs/configuration/overview) flags type: webhook as invalid. Precedent: the rag toolset change updated the schema in the same PR |
Item 1: inline suggestions below remove the references. If #3632 is expected to merge first, rebasing on top of it works too; as long as merge order is undefined, published docs should not reference a toolset that fails to load.
Item 2: add "webhook" in two places:
definitions/Toolset/properties/type/enumdefinitions/Toolset/anyOf[1]/properties/type/enum(variant listing types with no extra required field; no dedicatedanyOfentry is needed since the toolset takes no options)
Non-blocking inline suggestions: reuse httpclient.DefaultToolHTTPTimeout, set the docker-agent User-Agent on outbound requests, list all providers in the tool description, consider honoring timeout: / allowed_domains: from the toolset config.
Context note: the Go workflow (ci.yml) currently fails repo-wide in 0s ("workflow file issue", setup-go with: indentation, also failing on main pushes), so task lint / task test did not run for this PR. Pre-existing breakage, not caused by this change, but Go tests were not executed by CI.
…ems on webhook tool
…ccording to latest changes
|
I like the idea but as is this toolset is just an http call, and we already have a toolset for this. Let's try and figure out how this toolset can really be a webhook |
|
👋 This PR has merge conflicts with the base branch. Please rebase or merge the latest base branch and resolve them. I've moved it to draft and added |
Yes, you’re right @rumpl. I’m currently working on the architecture to make this something more flexible that we can build on and extend, rather than just another HTTP call wrapper. I’m also thinking about capabilities like outbound webhooks and how we can make the toolset a more proper webhook-based integration point with better extensibility. I’ll push the new implementation soon so we can review the direction and iterate on it. |
…atures tunning up the webhook tool design logic
… for new features of webhook tool
…igs into conf docs
…into agent-schema.json
|
@rumpl @Sayt-0 Thank you both - the review changed my mind on the design, so this is a rework rather than a patch. Heads-up: the tool's interface changed ( @rumpl - "the agent can't know the URL" / "we need at least some config"You were right, and it is true for every provider, not just Slack: the webhook URL is the credential (Slack and Mattermost embed a secret path, Discord a token, IFTTT a key, Telegram a bot token). Having the model supply it only ever worked if a user pasted a secret into the prompt. The destination is now deployment configuration, following the toolsets:
- type: webhook
webhook_config:
provider: slack
url: ${env.SLACK_WEBHOOK_URL}Token-based endpoints are covered by provider: telegram
url: https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage
chat_id: "123456789" provider: generic
url: https://alerts.example.com/notify
headers:
Authorization: Bearer ${env.ALERTS_TOKEN}
@rumpl - "as is this is just an http call, we already have a toolset for this"Also correct, and the sharpest point: once the URL moves into config, what was left was
I also considered making it a real inbound webhook (receive events → wake the agent), which is the truest reading of "webhook" and the one gap There is a deliberate behaviour trade-off worth flagging: delivery is fire-and-forget, so on success the agent gets no confirmation - only failures call back. That is correct webhook semantics, but happy to make async opt-in if you'd rather the agent always receive a verdict. @Sayt-0 - your points
CI noteSince the Go jobs still don't run, I installed golangci-lint v2.12.2 and ran the repo's
Do let me know if any change is required please. |
Why
|
api toolset |
webhook toolset |
|
|---|---|---|
| Question it answers | "Call this endpoint and give me the response." | "Make sure this notification arrives." |
| Model supplies | request args | a message |
| Execution | one request, synchronous | queued, retried, out-of-band |
| Failure | returns an error, agent must cope | retried; agent is called back only if it ultimately fails |
What api structurally cannot do
Verified against pkg/tools/builtin/api/api.go: it fires exactly one request and returns the body - it contains no retry, backoff, 429 or Retry-After handling.
1. At-least-once delivery
Transient failures (429, 5xx, network errors) are retried with exponential backoff (pkg/backoff), honouring the server's Retry-After (capped by backoff.MaxRetryAfterWait, so a misbehaving server can't stall the agent). A 4xx is classified permanent and fails immediately instead of burning retries.
With api, a transient blip means the notification is simply lost unless the model happens to notice and retry by hand - costing turns and doing it inconsistently.
2. Non-blocking delivery + failure callback - the structural difference
api is request/response: it cannot return before the call completes, and it cannot tell you anything afterwards. send_webhook returns as soon as the message is queued and delivers in the background, so a slow or retrying endpoint never stalls a turn. If delivery ultimately fails, the agent is woken via Runtime.Recall - the same primitive background_jobs uses.
This is not expressible in api_config at any setting. It is a different execution model, and it is what "webhook" means operationally: hand it over, it gets delivered, you hear about it only if it doesn't.
3. Storm protection
Agents loop. An identical message to the same destination within a short window is suppressed, and deliveries are rate limited, so a retry loop cannot flood a channel or burn the provider's rate limit. api has no memory between calls and no throttle.
4. Provider semantics
Each service's wire format is applied for the agent (Slack/Mattermost/Rocket.Chat/Google Chat/Teams {"text"}, Discord {"content"}, IFTTT {"value1..3"}, Telegram {"chat_id","text"}), and the result is normalised to delivered / transient / permanent. With api the user hand-writes each payload template and the model has to guess from a raw body whether it worked.
5. Credential shape
The destination is deployment config, not a model choice: every provider's webhook URL is the credential (Slack/Mattermost a secret path, Discord a token, IFTTT a key, Telegram a bot token). The model supplies only the message, never sees the URL, and the URL is never echoed into results or error messages. api would work here, but it leaves the endpoint choice in the tool surface.
Summary
api = "make an HTTP request." webhook = "deliver this notification reliably to a destination I configured." The overlap is the byte on the wire; everything that matters - retries, backoff, Retry-After, dedupe, rate limiting, async hand-off, failure callback, provider payloads - lives outside what api_config can express.
Every one of these is covered by unit tests with a fake HTTP doer, a fake runtime, and an injected clock - no network, no sleeping.
Closes #3640
Summary
New built-in
webhooktoolset —send_webhook(url, message, provider?)— that POSTs a provider-shaped JSON payload to a webhook and reports delivery status. Providers:slack,discord,ifttt,telegram,mattermost,rocketchat,googlechat,teams,generic. Reuseshttpclient.NewSafeClient(refuses non-public IPs). Seecontribution/webhook/ISSUE.mdfor motivation and payload shapes.Changes
pkg/tools/builtin/webhook/webhook.gobuildPayload, injectablehttpDoer.pkg/tools/builtin/webhook/webhook_test.gopkg/teamloader/toolsets/toolsets.gowebhookcreator.pkg/teamloader/toolsets/catalog.godocs/configuration/tools/index.md,docs/tools/webhook/index.mdTesting
go test ./pkg/tools/builtin/webhook/ ./pkg/teamloader/toolsets/Payload construction is a pure function and the HTTP client is injected, so every path is covered with a fake doer — no network.
gofmt -landgo vetclean. (CI runstask lint/task test.)Notes