feat(app): manage AI agents and wire them to alerts - #3123
jordan-simonovski wants to merge 6 commits into
Conversation
🦋 Changeset detectedLatest commit: e8d0181 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryAdds UI for provisioning and importing managed AI agents, selecting agents alongside webhooks as alert notification targets, and displaying resolved target names and icons.
Confidence Score: 4/5The PR should not merge until the new forbidden type assertion and the outstanding semantic-color requirement are corrected; the stale payload documentation is non-blocking but should also be updated. The alert form now introduces an Files Needing Attention: packages/app/src/components/Alerts.tsx, packages/app/src/components/TeamSettings/ImportAgentForm.tsx, packages/app/src/components/TeamSettings/AgentsSection.tsx, packages/app/src/components/TeamSettings/CreateAgentForm.tsx, packages/app/src/components/TeamSettings/agentForms.ts, docs/ai-agent-alert-investigations.md
|
| Filename | Overview |
|---|---|
| packages/app/src/components/alerts/NotificationTargetSelect.tsx | Adds the unified, feature-gated webhook and agent picker with duplicate prevention, search, unavailable-target handling, and validated composite values. |
| packages/app/src/components/Alerts.tsx | Integrates the unified picker into alert channel arrays, but its dynamic field path uses a repository-forbidden type assertion. |
| packages/app/src/components/TeamSettings/ImportAgentForm.tsx | Adds agent import and a generated manual setup script with base-path-aware MCP URL handling; the outstanding semantic notification-color finding remains. |
| packages/app/src/components/TeamSettings/AgentsSection.tsx | Adds feature-gated agent listing and deletion while retaining raw notification colors covered by the outstanding previous thread. |
| packages/api/src/tasks/checkAlerts/transports/agent.ts | Removes rendered alert bodies from agent payloads to reduce attacker-influenced content while retaining the source query needed for investigation. |
| packages/api/src/services/anthropicAgents.ts | Accepts imported agents when any configured MCP server matches this instance. |
| docs/ai-agent-alert-investigations.md | Documents managed-agent setup and dispatch, but its payload example still includes the now-omitted alert body. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Settings[Team integrations] --> Manage[Create or import agent]
Manage --> Anthropic[Anthropic managed agent]
Editor[Alert editor] --> Picker[Notification target picker]
Picker --> Webhook[Webhook channel]
Picker --> Agent[Agent channel]
Agent --> Anthropic
Anthropic --> MCP[ClickStack MCP server]
MCP --> Telemetry[Logs, traces, and metrics]
Reviews (16): Last reviewed commit: "fix(app): parse the notification target ..." | Re-trigger Greptile
Deep ReviewScope: UI to create/import/list/delete managed AI agents in Team Settings and select them alongside webhooks as alert notification targets, feature-flagged off by default. No ship-blockers. The feature is gated off by default ( ✅ No critical issues found. 🟡 P2 — recommended
🔵 P3 nitpicks (8)
Reviewers (4): agent-native, testing, project-standards, security. Testing gaps:
|
| onClick={open} | ||
| > | ||
| Add New Incoming Webhook | ||
| Add new notification target |
There was a problem hiding this comment.
🔵 minor — "Add new notification target" only creates a webhook
The button (and its new IconBellPlus) promises any target but opens a modal containing only WebhookForm; with agents enabled a user looking to add an agent lands on a webhook form. Label it "Create a webhook" to match the modal title, or route it to both creation paths.
| // configured agent row (which has no webhookId) is never mistaken for a | ||
| // free slot, and vice versa. | ||
| const placeCreatedTarget = (value: FieldArray<T, ArrayPath<T>>) => { | ||
| const emptyIndex = selectedTargetKeys.findIndex(key => key == null); |
There was a problem hiding this comment.
🔵 minor — placeCreatedTarget can overwrite a channel row the list deliberately doesn't render
channelTargetKey returns null for any row whose type isn't webhook/agent, so a fork's e.g. {type:'email'} row — which the map at line 105 intentionally renders as nothing — counts as the first "empty" slot and is destroyed by update(emptyIndex, ...) when a webhook is created. Treat a row as empty only when its type is webhook/agent and its id is unset.
| @@ -0,0 +1,28 @@ | |||
| import { | |||
There was a problem hiding this comment.
🔵 minor — The only new UI test pins a static constant table; the new interactive components have none
agentPresets.test.ts asserts labels are non-empty and briefs exceed 20 characters, which no plausible regression fails, while AgentsSection (delete confirm + 409 error surfacing), ImportAgentForm (the verified: false warning path and the generated snippet) and AddAgentModal's create-disabled branch are untested — the add-agent-mode/agent-preset testids they added are referenced nowhere. Add a render test for at least the delete-confirm and unverified-import paths.
|
|
||
| // The picker shows the chosen agent, proving the row's value flipped to | ||
| // the agent shape (the select derives its value from the channel object). | ||
| await waitFor(() => |
There was a problem hiding this comment.
🔵 minor — The agent-selection test asserts the displayed label, not the channel object that gets saved
The PR's core claim is that a row round-trips to {type:'agent', agentId}; the test only checks the input renders "SRE Responder". Wrap the harness in handleSubmit (as the validation test at line 241 already does) and assert the submitted channels[0] equals {type:'agent',agentId:'a1'} with no stray webhookId.
| agent, | ||
| onDeleted, | ||
| }: { | ||
| agent: { |
There was a problem hiding this comment.
🔵 minor — AgentRow re-declares the managed-agent shape instead of importing it
ManagedAgentData in packages/app/src/api.ts:48 already describes these fields; export it and use Pick<ManagedAgentData, ...> here rather than a second hand-written copy (Code Style → "Define and import reusable named types instead of repeating verbose types").
| <Tabs.Panel value="claude"> | ||
| <Card> | ||
| {agents.length === 0 ? ( | ||
| <EmptyState description="No agents yet. Add one to use it as an alert notification channel."> |
There was a problem hiding this comment.
🔵 minor — EmptyState is rendered with no title
The conventions' EmptyState section says to treat title as a short headline matching listing pages ("No … yet", no trailing period) with full sentences in description; split the copy into title="No agents yet" and description="Add one to use it as an alert notification channel.".
PR Review8 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 7 minor 6 posted as inline comment(s) on the changed lines. 2 unchanged from an earlier push (already inline above). Severity is the reviewer's own estimate and is used for ordering, not filtering. |
f95e195 to
2322cee
Compare
E2E Test Results✅ All tests passed • 358 passed • 1 skipped • 1547s
Tests ran across 4 shards in parallel. |
🔴 Tier 4 — CriticalTouches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
2322cee to
6ec0350
Compare
|
Both fixed. MCP URL drops base path — real bug, and this repo has been bitten by it before. Built from Badge uses raw colour — dropped the |
| export const IS_MTVIEWS_ENABLED = false; | ||
| export const IS_SESSIONS_ENABLED = true; | ||
| export const IS_PROMQL_ENABLED = env('NEXT_PUBLIC_ENABLE_PROMQL') === 'true'; | ||
| export const IS_MANAGED_AGENTS_ENABLED = |
There was a problem hiding this comment.
🔵 minor — New env-configurable flags missing from the hdxDebug feature snapshot
config.ts:74 instructs that any env-configurable flag be added to the feature snapshot, but IS_MANAGED_AGENTS_ENABLED / IS_MANAGED_AGENT_CREATE_ENABLED are absent from staticFeatures in hdxDebug.ts:83-87, so window.hdx can't tell you whether a deployment has agents on. Add them alongside promql, llmCost and alertDetails.
| >; | ||
| }; | ||
|
|
||
| type ManagedAgentData = { |
There was a problem hiding this comment.
🔵 minor — Managed-agent API response type hand-written in the app and re-declared in AgentsSection
ManagedAgentData is defined locally (and not exported), then a subset of the same shape is spelled out again for AgentRow's props (AgentsSection.tsx:28-36); the repo's pattern for API payloads is one shared definition in common-utils used by both sides — WebhooksApiResponse / WebhookApiData (packages/common-utils/src/types.ts:663, 2786) typing both packages/api/src/routers/api/webhooks.ts:144 and api.useWebhooks. Define the agent response schema/type once in common-utils and import it in api.ts, the route, and AgentsSection.
| @@ -0,0 +1,28 @@ | |||
| import { | |||
There was a problem hiding this comment.
🔵 minor — The only new Team-settings test pins a static table; the agent UI itself is untested
This suite asserts that preset labels are non-empty and briefs exceed 20 characters — it would pass any regression in the code that uses them, while AgentsSection, AddAgentModal, CreateAgentForm and ImportAgentForm have no tests at all. Cover the behaviour that can actually break: the create tab hidden when IS_MANAGED_AGENT_CREATE_ENABLED is false, the delete confirmation copy/label differing for imported agents, and buildManualSetupScript emitting the shared AUTO_ALLOWED_MCP_TOOLS/AGENT_TOOLSET policy (the snippet is the only thing that sets an imported agent's toolset).
6ec0350 to
dad82e3
Compare
dad82e3 to
1ec02a9
Compare
609e903 to
e0cf29d
Compare
e0cf29d to
cc9ca40
Compare
| // configured agent row (which has no webhookId) is never mistaken for a | ||
| // free slot, and vice versa. | ||
| const placeCreatedTarget = (value: FieldArray<T, ArrayPath<T>>) => { | ||
| const emptyIndex = selectedTargetKeys.findIndex(key => key == null); |
There was a problem hiding this comment.
🔵 minor — A channel row of an unhandled type is treated as a free slot and silently overwritten when a webhook is created
channelTargetKey returns null both for "nothing selected" and for "a type this repo doesn't render", so placeCreatedTarget picks the latter as the empty slot. With channels [{type:'email',emailRecipients:[…]}, {type:'webhook',webhookId:'w1'}] — the exact state AlertChannelForm.test.tsx:190 constructs and the file's own comment says is skipped so "the rest of the list still works" — clicking "Add new notification target" and creating a webhook calls update(0, …) and destroys the email channel, which isn't even rendered so the user never sees it go. Decide emptiness from the row's own type instead: only rows whose type is webhook/agent and whose id is blank are free slots.
| <Text size="md">AI agents</Text> | ||
| <Text size="xs" c="dimmed" mt={4}> | ||
| Connect a cloud AI agent that investigates alerts through the ClickStack | ||
| MCP server. Add it to an alert as a 🤖 notification channel; each firing |
There was a problem hiding this comment.
🔵 minor — Section copy tells users to look for a "🤖 notification channel" that no longer exists in the UI
Nothing in the notification picker is labelled with 🤖: NotificationTargetSelect groups agents under "AI agents" with a Claude glyph, and the channel-type select that this PR removes never carried an emoji either. Describe the real affordance ("add it to an alert as a notification target — agents appear beside webhooks in the picker"); docs/ai-agent-alert-investigations.md:4 needs the same correction.
| export const IS_MTVIEWS_ENABLED = false; | ||
| export const IS_SESSIONS_ENABLED = true; | ||
| export const IS_PROMQL_ENABLED = env('NEXT_PUBLIC_ENABLE_PROMQL') === 'true'; | ||
| export const IS_MANAGED_AGENTS_ENABLED = |
There was a problem hiding this comment.
🔵 minor — New env-configurable flags are missing from the hdxDebug feature snapshot
config.ts:74 states the rule for this file — "When adding an env-configurable flag (one whose value varies by deployment), add it to the feature snapshot in hdxDebug.ts" — but staticFeatures in packages/app/src/hdxDebug.ts:83-87 still lists only promql/llmCost/alertDetails, so a bug report from a deployment can't show whether managed agents were on. Add managedAgents: IS_MANAGED_AGENTS_ENABLED and managedAgentCreate: IS_MANAGED_AGENT_CREATE_ENABLED there (and to the config mock in src/__tests__/hdxDebug.test.ts).
| agent, | ||
| onDeleted, | ||
| }: { | ||
| agent: { |
There was a problem hiding this comment.
🔵 minor — AgentRow re-declares the managed-agent response type instead of importing it
ManagedAgentData in packages/app/src/api.ts:48 already describes exactly these fields but isn't exported, so AgentRow restates seven of them inline — two definitions to keep in sync, against the REQUIRED "define and import reusable named types instead of repeating verbose types" rule. Export ManagedAgentData from api.ts and type the prop as Pick<ManagedAgentData, …> (the same applies to the loose channel shape restated in Alerts.tsx:60 and NotificationTargetSelect.tsx:37, which duplicates the unexported alertsPageItemChannelSchema in packages/common-utils/src/types.ts:2645).
| expect(new Set(values).size).toBe(values.length); | ||
| expect(AGENT_PRESETS.every(p => p.label.trim().length > 0)).toBe(true); | ||
| }); | ||
|
|
There was a problem hiding this comment.
🔵 minor — The only new test covers a static data table; the agent management UI has none
These assertions (values distinct, labels non-empty, briefs longer than 20 chars) can't fail for any plausible regression, while the logic this PR adds goes untested: AddAgentModal's create/import gating on IS_MANAGED_AGENT_CREATE_ENABLED, ImportAgentForm's unverified-import warning and its mcpServerUrl ?? origin+BASE_PATH fallback in the copied snippet, and AgentRow's imported-vs-owned confirm copy and delete. Add component tests for those (mocking @/useConfirm per the repo convention) rather than pinning preset text.
|
|
||
| const webhookList = useMemo(() => webhooks?.data ?? [], [webhooks]); | ||
| const agentList = useMemo( | ||
| () => (IS_MANAGED_AGENTS_ENABLED ? (agents?.data ?? []) : []), |
There was a problem hiding this comment.
🔵 minor — The flag check on agentList cannot matter
The query one line above is already enabled: IS_MANAGED_AGENTS_ENABLED, and every other caller of useManagedAgents is gated on the same flag (AgentsSection returns null, ImportAgentForm only renders inside it), so with the flag off the shared ['managed-agents'] cache is always empty and agents?.data ?? [] is already []. Drop the ternary and keep agents?.data ?? [].
cc9ca40 to
54e76a0
Compare
54e76a0 to
b831342
Compare
| // channel written through the API while this build has agents switched off | ||
| // — would otherwise render as a blank required row, and the next pick would | ||
| // silently overwrite it. Show it instead, so replacing it is a choice. | ||
| const { options, isDangling } = useMemo(() => { |
There was a problem hiding this comment.
🔵 minor — A valid webhook/agent is labelled "(unavailable)" while the target lists are still loading (or if the fetch fails)
data is empty until api.useWebhooks / api.useManagedAgents resolve, so on first open of an alert editor with a cold cache (e.g. the saved-search alert modal or the dashboard tile editor, neither of which renders AlertPropertiesSummary first to warm the shared query) an alert whose webhookId is perfectly valid renders as "Webhook (unavailable)" with the fallback bell icon until the fetch lands — and stays that way permanently if the request errors. Gate the dangling branch on the queries having settled (isSuccess/isFetched from both hooks) and keep the plain selected value until then.
| // configured agent row (which has no webhookId) is never mistaken for a | ||
| // free slot, and vice versa. | ||
| const placeCreatedTarget = (value: FieldArray<T, ArrayPath<T>>) => { | ||
| const emptyIndex = selectedTargetKeys.findIndex(key => key == null); |
There was a problem hiding this comment.
🔵 minor — placeCreatedTarget overwrites a configured channel row whose type this repo doesn't render
channelTargetKey returns null for any row that isn't a webhook/agent, so a downstream fork's configured row (e.g. {type:'email', emailRecipients:[…]} — the case the skip at line 105 and the foreignChannel test at AlertChannelForm.test.tsx:190 exist for) is treated as a free slot and silently replaced by the newly created webhook, destroying a channel the user cannot even see in this UI. The comment above claims emptiness is keyed so this can't happen; make it true by only reusing rows whose type is webhook/agent and whose key is null.
| onClick={open} | ||
| > | ||
| Add New Incoming Webhook | ||
| Add new notification target |
There was a problem hiding this comment.
🔵 minor — "Add new notification target" only ever creates a webhook
The button was relabelled from "Add New Incoming Webhook" to "Add new notification target", but it still opens a modal titled "Create a webhook" containing only WebhookForm — with agents enabled a user who wants to add an agent lands on a webhook form with no route to one. Either label it "Create a webhook" to match what it does, or route agent creation through it too (Team settings is currently the only place an agent can be added).
| agent, | ||
| onDeleted, | ||
| }: { | ||
| agent: { |
There was a problem hiding this comment.
🔵 minor — The managed-agent shape is re-typed inline instead of reusing ManagedAgentData
AgentRow restates seven fields that already exist as ManagedAgentData in packages/app/src/api.ts:48 (the response type the list comes from), so the two drift independently. Export ManagedAgentData from api.ts and type the prop as agent: ManagedAgentData (or Pick<ManagedAgentData, …>).
| jq -n --arg system "$SYSTEM" --arg url "$MCP_URL" \\ | ||
| --argjson read "$READ_TOOLS" --argjson builtin "$BUILTIN_TOOLS" '{ | ||
| name: "ClickStack SRE Responder", | ||
| model: "claude-opus-4-8", |
There was a problem hiding this comment.
🔵 minor — The setup snippet hardcodes a model that duplicates MODEL_OPTIONS[0]
The snippet is deliberately generated from the shared tool-policy constants so it can't drift from what the API provisions, but model: "claude-opus-4-8" is a second literal of the default model already declared in packages/app/src/components/TeamSettings/agentForms.ts:6. Interpolate MODEL_OPTIONS[0].value so the create form and the import snippet can't disagree.
| ); | ||
| } | ||
|
|
||
| export default function AgentsSection() { |
There was a problem hiding this comment.
🔵 minor — The new AI agents settings UI has no component test; the only new test covers a static table
agentPresets.test.ts asserts properties of a constant array, while the behaviour this PR adds — the flag gating in AgentsSection/AddAgentModal, the delete confirmation copy and imported-vs-created wording, the unverified-import warning notification — is untested, even though sibling settings sections have tests (ApiKeysSection.test.tsx, McpServerSection.test.tsx). Add an AgentsSection test in the same style (mock @/api and @/useConfirm, assert the confirm arguments and that the create tab is hidden when IS_MANAGED_AGENT_CREATE_ENABLED is false).
Turns the agent channel into something a team can actually use. Team settings -> Integrations gains an AI agents section: provision one against your Anthropic account, or import one you wrote yourself by its ID, with the setup commands behind a disclosure for anyone who wants to write their own system prompt. Pick a type — general, database, Kubernetes, application errors or latency — and its brief is appended to the standing prompt, so a specialist can run alongside a general responder. The list names whose ClickStack access key each agent carries, since that is what it investigates as. The alert notification picker becomes one searchable list of webhooks and agents rather than a webhook-only dropdown, so adding an investigation to an alert that already pages someone is a second row, not a choice between the two. Agent targets are named and iconed wherever webhooks already were, on the alerts list and the alert detail page. The setup snippet is generated from the same tool-policy constants the API provisions with, because nothing inspects an imported agent's toolset afterwards — what that snippet creates is what runs unattended. Everything is hidden unless NEXT_PUBLIC_HDX_MANAGED_AGENTS_ENABLED is set, with the create tab behind NEXT_PUBLIC_HDX_MANAGED_AGENTS_ALLOW_CREATE.
The setup snippet handed users `${origin}/api/mcp`, which drops the prefix on
a deployment hosted under a base path — an agent created from it could not
reach ClickStack when an alert fired, and the failure would only show up at
3am. Built from BASE_PATH now, like everything else that has to survive a
prefixed deployment.
Also drops a raw palette colour from the "coming soon" badge in favour of the
default token.
… MCP URL A configured target that isn't in the fetched list — a deleted webhook, or an agent channel written through the API while this build has agents switched off — rendered as a blank required row, and the next selection silently replaced it. It is shown as "unavailable" now, so replacing it is a choice rather than an accident. The setup snippet derived MCP_URL from the browser origin, which is wrong wherever an operator has set HDX_MANAGED_AGENTS_MCP_URL — the vault credential is bound to that URL, not this one. The server reports the URL it provisions for and the snippet uses it, falling back to the origin only for the default. Also stops the literal string "undefined" appearing in an agent's search keywords when its model is unknown.
The composite `kind:id` value was split and the prefix asserted to be a target kind, so a stored row with any other prefix would have been read as a webhook. It is parsed now and an unrecognised value is rejected rather than coerced. The control was also declared as Control<any>, which gave up type checking for every caller. It is generic over the form type like AlertNoteField beside it, with the one dynamic array path narrowed where it is built, since react-hook-form has no type for "element of this array path".
b831342 to
e8d0181
Compare
|
Both remaining rule findings fixed.
Raw notification colours — not taking this one, and it is a judgement call rather than a refusal. |
| takenWebhookIds={selectedWebhookIds.filter( | ||
| (id, i) => i !== index && !!id, | ||
| // react-hook-form cannot express "element of this array path" | ||
| // as a Path<T>, so the one dynamic path is narrowed here rather |
There was a problem hiding this comment.
The new as Path<T> assertion bypasses type checking for the dynamically built form path. This violates the repository directive to avoid as casts in favor of inference or satisfies, so the repository requirement must be satisfied before merging.
Context Used: CLAUDE.md (source)
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!
| "source": "clickstack", | ||
| "schema_version": "1", | ||
| "prompt": "A ClickStack alert fired. Investigate the root cause…", | ||
| "alert": { "id": "…", "event_id": "…", "status": "firing", "type": "search", "title": "…", "body": "…", "link": "…" }, |
| // channel written through the API while this build has agents switched off | ||
| // — would otherwise render as a blank required row, and the next pick would | ||
| // silently overwrite it. Show it instead, so replacing it is a choice. | ||
| const { options, isDangling } = useMemo(() => { |
There was a problem hiding this comment.
🟠 major — A valid webhook/agent is labelled "(unavailable)" while its list is still loading
data is empty until api.useWebhooks (packages/app/src/api.ts:500 — no placeholderData/initialData) resolves, so on the first render of any alert editor with a cold cache known is false and the row renders the "Currently set" group with Webhook (unavailable) (and a bell instead of the service icon) for a webhook that is perfectly fine. Reachable on the saved-search alert modal and the chart editor, where nothing has primed that query beforehand — on the alerts page AlertPropertiesSummary warms it first, which is probably why it wasn't noticed. Only compute the dangling entry once the backing lists have actually resolved: webhooks !== undefined && (!IS_MANAGED_AGENTS_ENABLED || agents !== undefined), falling back to data (blank row, as before) until then.
| // configured agent row (which has no webhookId) is never mistaken for a | ||
| // free slot, and vice versa. | ||
| const placeCreatedTarget = (value: FieldArray<T, ArrayPath<T>>) => { | ||
| const emptyIndex = selectedTargetKeys.findIndex(key => key == null); |
There was a problem hiding this comment.
🔵 minor — placeCreatedTarget still overwrites a configured row whose channel type this repo does not render
channelTargetKey returns null for any row that is neither a filled webhook nor a filled agent, so a downstream fork's configured row (e.g. {type:'email', emailRecipients:[…]} — the exact shape the suite covers in "renders nothing for a row of a type this repo does not handle") is found by findIndex(key => key == null) and destroyed by update(emptyIndex, …) when a webhook is created from the modal. The comment above claims emptiness is now keyed so a configured row is "never mistaken for a free slot", but that only holds for agents. Skip rows whose type is not webhook/agent when looking for the free slot (e.g. search channels directly rather than selectedTargetKeys).
| agent, | ||
| onDeleted, | ||
| }: { | ||
| agent: { |
There was a problem hiding this comment.
🔵 minor — Agent shape re-declared inline instead of reusing ManagedAgentData
AgentRow's prop type restates seven fields of ManagedAgentData (packages/app/src/api.ts:48), so the two can drift silently — the row would keep compiling against a field the API response no longer returns. Export ManagedAgentData from api.ts and use agent: ManagedAgentData (or Pick<ManagedAgentData, …>) here, per the repo's "import reusable named types instead of repeating verbose types" rule.
| const hasTargets = options.length > 0; | ||
|
|
||
| // Matches the visible name OR the hidden kind keywords ("slack", "ai", ...). | ||
| const filter: SelectProps['filter'] = ({ options, search }) => { |
There was a problem hiding this comment.
🔵 minor — Grouped combobox filter duplicates sourceSelectFilter
sourceSelectFilter (packages/app/src/components/sourceSelectUtils.tsx:125) already walks ComboboxParsedItem[], filters group items against a per-item haystack, drops emptied groups, and honours Mantine's limit; this reimplements that traversal and silently drops the limit handling. Parameterize the existing helper with the haystack function (label + group label here, label + keyword map there) and import it, rather than keeping a second grouped-filter implementation.
| "source": "clickstack", | ||
| "schema_version": "1", | ||
| "prompt": "A ClickStack alert fired. Investigate the root cause…", | ||
| "alert": { "id": "…", "event_id": "…", "status": "firing", "type": "search", "title": "…", "body": "…", "link": "…" }, |
There was a problem hiding this comment.
🔵 minor — Documented agent payload advertises an alert.body field that is never sent
buildAgentPrompt (packages/api/src/tasks/checkAlerts/transports/agent.ts:39-46) emits id, event_id, status, type, title, link and explicitly omits the rendered body — there's a comment saying the matched rows are deliberately excluded as the largest slice of attacker-influenced text. Someone writing agent instructions against context/alert.body from this doc gets undefined. Drop "body": "…" from the example (and, if useful, note that the body is intentionally absent).
| @@ -0,0 +1,28 @@ | |||
| import { | |||
There was a problem hiding this comment.
🔵 minor — The only new test pins a static constant; the new behaviour is untested
agentPresets.test.ts asserts that a literal array has unique values, non-empty labels and a blank default — all restatements of the file it imports, and none of it would catch a regression in the feature. Meanwhile AddAgentModal, CreateAgentForm (preset → instructions on the create request), ImportAgentForm (the mcpServerUrl fallback and the generated snippet) and AgentsSection's delete confirmation have no tests at all, and neither does NotificationTargetSelect's dangling-target path — the behaviour the changeset calls out by name. At minimum add a case pinning that a stored target absent from both lists renders as "(unavailable)" and survives without being overwritten, and that the picked preset's text reaches useCreateManagedAgent.
Turns the agent channel into something a team can use without touching the API. Team settings -> Integrations gains an AI agents section, and the alert notification picker offers agents alongside webhooks.
Enabling it
Off by default. Read at runtime, so an env change and a restart is enough — no rebuild.
NEXT_PUBLIC_HDX_MANAGED_AGENTS_ENABLEDNEXT_PUBLIC_HDX_MANAGED_AGENTS_ALLOW_CREATEPair with
HDX_MANAGED_AGENTS_ENABLEDandHDX_MANAGED_AGENTS_ALLOW_CREATEon the API — see #3120.Key decisions
The webhook-only channel form is replaced by one grouped, searchable target picker rather than adding a second control beside it. An alert's targets are a list of things to notify; splitting the UI by kind would have made "page me and investigate" read as two features instead of two rows.
The import snippet is generated from the same tool-policy constants the API provisions with. Nothing inspects or rewrites an imported agent's toolset afterwards, so whatever that snippet creates is what runs unattended — it cannot be allowed to drift from the server's posture.
Only the agent's ID is asked for on import. Its name and model are read from the agent object rather than retyped, so they cannot disagree with what Anthropic will actually run.
Impact
Nothing appears until the flags are set. Deleting an agent goes through the standard confirmation and is refused while an alert still targets it.
Carries its own changeset, scoped to the UI. Each PR in the stack has one.
Implementation detail
NotificationTargetSelectreplacesWebhookChannelForm, keeping webhook selection, duplicate disabling, service icons and validation-error surfacing; option values are composite so a target round-trips back to the right channel shape.useNotificationTargetsinAlertPropertiesSummarynow branches onchannel.typeand carries a resolved icon rather than a webhook service, so an agent channel no longer renders as "Webhook" with a webhook icon. The agents query is gated on the feature flag, so a deployment with agents off makes no extra request.ALERT_CHANNEL_OPTIONSis removed here along with its last consumer.Verified with
make ci-lint,make ci-unit,make dev-int FILE=alertsandFILE=gent. This branch's tree is byte-identical to the single PR this stack replaces, which was verified the same way.