Skip to content

feat(app): manage AI agents and wire them to alerts - #3123

Open
jordan-simonovski wants to merge 6 commits into
jordansimonovski/agent-alert-dispatchfrom
jordansimonovski/agent-alert-ui
Open

jordan-simonovski wants to merge 6 commits into
jordansimonovski/agent-alert-dispatchfrom
jordansimonovski/agent-alert-ui

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Top of the stack — #3120 -> #3122 -> this. Review the others first; this PR's diff is against #3122.

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.

  • Provision an agent, or import one you wrote yourself by its Anthropic ID, with the setup commands behind a disclosure for anyone who wants to write their own system prompt.
  • Give an agent a type — general, database, Kubernetes, application errors, latency — whose brief is appended to the standing prompt, so a specialist can run alongside a general responder.
  • See whose ClickStack access key each agent carries, since that is what it investigates as.
  • Add an agent to an alert from one searchable list of webhooks and agents, so pairing an immediate page with an investigation is a second row rather than a choice between the two.
  • Agent targets are named and iconed on the alerts list and the alert detail page, wherever webhooks already were.

Enabling it

Off by default. Read at runtime, so an env change and a restart is enough — no rebuild.

Variable Effect
NEXT_PUBLIC_HDX_MANAGED_AGENTS_ENABLED The Team settings section and the agent options in the notification picker.
NEXT_PUBLIC_HDX_MANAGED_AGENTS_ALLOW_CREATE The "create" tab in the add-agent dialog. Import stays available without it.

Pair with HDX_MANAGED_AGENTS_ENABLED and HDX_MANAGED_AGENTS_ALLOW_CREATE on 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

NotificationTargetSelect replaces WebhookChannelForm, 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.

useNotificationTargets in AlertPropertiesSummary now branches on channel.type and 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_OPTIONS is removed here along with its last consumer.

Verified with make ci-lint, make ci-unit, make dev-int FILE=alerts and FILE=gent. This branch's tree is byte-identical to the single PR this stack replaces, which was verified the same way.

@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e8d0181

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

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

@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 15, 2026 6:25am UTC
hyperdx-storybook Ready Ready Preview Sep 15, 2026 6:25am UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds UI for provisioning and importing managed AI agents, selecting agents alongside webhooks as alert notification targets, and displaying resolved target names and icons.

  • Adds feature-gated agent management under team integrations.
  • Introduces a grouped, searchable webhook and agent target picker.
  • Documents managed-agent setup and alert investigation behavior.
  • Recent updates harden upstream error handling, support agents with multiple MCP servers, and omit rendered notification bodies from investigation payloads.

Confidence Score: 4/5

The 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 as Path<T> assertion contrary to the repository’s type-safety requirement. The unresolved previous notification-color finding also remains present: raw green, yellow, and red Mantine colors are still used in ImportAgentForm.tsx, AgentsSection.tsx, CreateAgentForm.tsx, and agentForms.ts. Separately, the documentation advertises an alert.body payload field that the latest dispatch code intentionally removed.

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

Important Files Changed

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]
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (16): Last reviewed commit: "fix(app): parse the notification target ..." | Re-trigger Greptile

Comment thread packages/app/src/components/TeamSettings/ImportAgentForm.tsx Outdated
Comment thread packages/app/src/components/TeamSettings/AgentsSection.tsx Outdated
@jordan-simonovski
jordan-simonovski added this pull request to stack #3124 September 14, 2026 05:35
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Scope: 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. NotificationTargetSelect replaces the deleted WebhookChannelForm; ALERT_CHANNEL_OPTIONS and its per-channel type selects are removed from three alert editors.

No ship-blockers. The feature is gated off by default (IS_MANAGED_AGENTS_ENABLED), and the prior-review threads that carried real bugs — MCP URL dropping BASE_PATH, the snippet guessing the MCP URL, the empty-picker silent-overwrite, the literal "undefined" search keyword, and the raw badge color — are all addressed in the current tree. What remains is test coverage and standards polish.

✅ No critical issues found.

🟡 P2 — recommended

  • packages/app/src/hdxDebug.ts:83 — the new env-configurable flags IS_MANAGED_AGENTS_ENABLED and IS_MANAGED_AGENT_CREATE_ENABLED are absent from the staticFeatures snapshot, so window.hdx cannot report whether a deployment has agents on, despite the config.ts directive requiring every env flag to be added there.
    • Fix: add managedAgents/managedAgentCreate entries to staticFeatures alongside promql, llmCost, and alertDetails.
    • project-standards
  • packages/app/src/components/TeamSettings/AgentsSection.tsx:42 — the new interactive agent components have no tests: delete-confirm (imported vs owned copy/label), the cancelled-confirm path, and the onError 409-surfacing path in AgentsSection, plus ImportAgentForm's verified:false warning branch and AddAgentModal's create-disabled default, are all uncovered and the added testids are referenced nowhere.
    • Fix: add render tests for the delete-confirm/409 path and the unverified-import path at minimum.
    • testing
  • packages/app/src/components/__tests__/AlertChannelForm.test.tsx:139 — the agent-selection test asserts only the displayed label "SRE Responder", not the saved channel object, so it would pass even if onChange wrote the wrong type or a stray webhookId.
    • Fix: submit via handleSubmit (as the validation test does) and assert channels[0] equals {type:'agent',agentId:'a1'}.
    • testing
🔵 P3 nitpicks (8)
  • packages/app/src/components/alerts/NotificationTargetSelect.tsx:31fromValue narrows the parsed prefix with an unchecked as TargetKind cast, which the repo code-style directive says to avoid; values are internally controlled so there is no runtime failure path today.
    • Fix: validate the prefix against the known kinds and return a discriminated result instead of asserting.
    • project-standards
  • packages/app/src/components/alerts/NotificationTargetSelect.tsx:57 — the control prop is typed Control<any>, defeating type-checking against the form shape.
    • Fix: parameterize the component over <T extends FieldValues> and type control: Control<T>.
    • project-standards
  • packages/app/src/api.ts:48ManagedAgentData is hand-written and unexported, then a subset is re-declared for AgentRow props in AgentsSection.tsx:28; the repo pattern for API payloads is one shared type in common-utils used by both sides.
    • Fix: define the managed-agent response type once in common-utils and import it in api.ts and the row component.
  • packages/app/src/components/TeamSettings/AgentsSection.tsx:185 — the EmptyState puts the headline in description with a trailing period and passes no title, diverging from the listing-page convention.
    • Fix: set title="No agents yet" and move the call-to-action sentence into description.
    • project-standards
  • packages/app/src/components/Alerts.tsx:153 — the "Add new notification target" button opens a webhook-only modal titled "Create a webhook", so with agents enabled a user seeking to add an agent lands on a webhook form.
    • Fix: relabel the button to match the webhook-only modal, or route it to both creation paths.
  • docs/ai-agent-alert-investigations.md — the new doc tells users to "switch the channel type to 'AI agent'", a control this same diff removes, and uses 🤖 wording that does not match the Claude glyph the picker and alert rows render.
    • Fix: drop the channel-type-switch clause and describe the grouped picker; reconcile the 🤖 wording with the rendered icon.
  • packages/app/src/components/TeamSettings/ImportAgentForm.tsx:33mcpUrl is interpolated unescaped into the copyable shell snippet (MCP_URL="${mcpUrl}"); it derives from server config validated only for an https:// prefix, so a value with shell metacharacters would execute when an operator runs the snippet (config-controlled, not externally exploitable).
    • Fix: validate the URL format or reject shell metacharacters before rendering it into the snippet.
    • security
  • packages/app/src/components/alerts/NotificationTargetSelect.tsx:132 — the isDangling "Currently set (unavailable)" branch for a deleted/unavailable stored target has no test, though its comment notes it exists to prevent silent overwrite.
    • Fix: add a test rendering a row whose stored id is absent from the fetched lists and assert the "Currently set" option and fallback icon.
    • testing

Reviewers (4): agent-native, testing, project-standards, security.

Testing gaps:

  • New agent management flows (create/import/delete, verified/unverified import, create-disabled deployment) have neither unit nor e2e coverage; added testids are unreferenced.
  • No assertion that buildManualSetupScript embeds the shared AUTO_ALLOWED_MCP_TOOLS/AGENT_TOOLSET constants, so snippet-vs-provisioning tool-policy drift would go unnoticed.
  • Coverage note: correctness, adversarial, previous-comments, kieran-typescript, api-contract, maintainability, reliability, and frontend-races reviewers did not return within the review window; the merge above combines the four returned personas with orchestrator-level code analysis, and the prior-comment threads were verified addressed directly against the current tree.

Comment thread packages/app/src/components/TeamSettings/ImportAgentForm.tsx Outdated
Comment thread packages/app/src/components/alerts/NotificationTargetSelect.tsx
onClick={open}
>
Add New Incoming Webhook
Add new notification target

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.

🔵 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);

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.

🔵 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.

Comment thread packages/app/src/components/alerts/NotificationTargetSelect.tsx Outdated
Comment thread docs/ai-agent-alert-investigations.md Outdated
@@ -0,0 +1,28 @@
import {

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.

🔵 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(() =>

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.

🔵 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: {

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.

🔵 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.">

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.

🔵 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.".

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

PR Review

8 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.

@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/agent-alert-ui branch from f95e195 to 2322cee Compare September 14, 2026 05:46
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 358 passed • 1 skipped • 1547s

Status Count
✅ Passed 358
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Sep 14, 2026
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches 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:

  • Large diff: 1229 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 19
  • Production lines changed: 1229 (+ 215 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/agent-alert-ui
  • Author: jordan-simonovski

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

Both fixed.

MCP URL drops base path — real bug, and this repo has been bitten by it before. Built from BASE_PATH now, so a deployment under a prefix hands out a reachable URL instead of one that only fails when an alert fires.

Badge uses raw colour — dropped the color="gray".

Comment thread packages/app/src/components/TeamSettings/ImportAgentForm.tsx Outdated
Comment thread docs/ai-agent-alert-investigations.md Outdated
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 =

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.

🔵 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.

Comment thread packages/app/src/api.ts
>;
};

type ManagedAgentData = {

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.

🔵 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 {

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.

🔵 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).

Comment thread packages/app/src/components/alerts/NotificationTargetSelect.tsx Outdated
Comment thread packages/app/src/components/alerts/NotificationTargetSelect.tsx Outdated
// 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);

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.

🔵 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

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.

🔵 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 =

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.

🔵 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: {

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.

🔵 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);
});

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.

🔵 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 ?? []) : []),

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.

🔵 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 ?? [].

@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/agent-alert-ui branch from cc9ca40 to 54e76a0 Compare September 14, 2026 21:44
@jordan-simonovski
jordan-simonovski removed this pull request from stack #3124 September 15, 2026 04:32
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/agent-alert-ui branch from 54e76a0 to b831342 Compare September 15, 2026 04:40
@jordan-simonovski
jordan-simonovski added this pull request to stack #3130 September 15, 2026 04:40
// 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(() => {

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.

🔵 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);

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.

🔵 minorplaceCreatedTarget 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

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.

🔵 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: {

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.

🔵 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",

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.

🔵 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() {

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.

🔵 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".
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/agent-alert-ui branch from b831342 to e8d0181 Compare September 15, 2026 06:21
@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

Both remaining rule findings fixed.

as TargetKind — the composite value is parsed now, and an unrecognised prefix is rejected instead of being coerced into a kind we do handle.

Control<any> — the component is generic over the form type, matching AlertNoteField beside it. One cast remains, moved to where the dynamic array path is built in Alerts.tsx: react-hook-form has no type for "element of this array path", so narrowing it at the single construction site beats widening the component's public API for every caller.

Raw notification colours — not taking this one, and it is a judgement call rather than a refusal. code_style.md:180 names Alert, Text, Button and ActionIcon; it does not cover notifications.show. There are 33 existing color: 'green'/'red' call sites in the app, including ApiKeysSection and TeamMembersSection in the same directory, so changing only the three agent ones makes this file inconsistent with its own neighbours. Worth doing app-wide as its own change, or not at all.

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

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.

P2 Form path bypasses typing

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!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

"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": "…" },

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.

P2 Payload example is stale

The setup guide shows alert.body in the dispatch payload, but the current dispatch code deliberately omits that field. Operators who extend agents or integrations from this example will expect data that is never sent.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

// 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(() => {

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.

🟠 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);

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.

🔵 minorplaceCreatedTarget 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: {

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.

🔵 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 }) => {

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.

🔵 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": "…" },

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.

🔵 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 {

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.

🔵 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.

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

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant