From 9c5b4e2bacddb0cb128672af5f8576e77d827e5c Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Sun, 28 Jun 2026 17:59:03 +1000 Subject: [PATCH 01/14] committing work in progress --- .changeset/claude-managed-agents-webhook.md | 23 + .changeset/managed-agents-provisioning.md | 16 + docs/alert-webhook-claude-managed-agents.md | 286 ++++++ packages/api/.env.development | 12 +- packages/api/src/api-app.ts | 1 + packages/api/src/config.ts | 6 + .../api/src/models/anthropicIntegration.ts | 43 + packages/api/src/models/managedAgent.ts | 50 + packages/api/src/routers/api/index.ts | 2 + packages/api/src/routers/api/managedAgents.ts | 178 ++++ packages/api/src/routers/api/webhooks.ts | 13 +- .../__tests__/anthropicAgents.test.ts | 147 +++ packages/api/src/services/anthropicAgents.ts | 165 ++++ .../__tests__/renderAlertTemplate.test.ts | 98 ++ .../api/src/tasks/checkAlerts/template.ts | 64 +- .../src/utils/__tests__/encryption.test.ts | 44 + packages/api/src/utils/encryption.ts | 62 ++ packages/app/.env.development | 1 + packages/app/src/TeamPage.tsx | 5 + packages/app/src/api.ts | 62 ++ packages/app/src/components/Alerts.tsx | 1 + .../components/TeamSettings/AgentsSection.tsx | 314 +++++++ .../components/TeamSettings/WebhookForm.tsx | 80 +- .../TeamSettings/WebhooksSection.tsx | 1 + packages/app/src/config.ts | 2 + .../api-capability-slos.json | 691 ++++++++++++++ .../api-instrumentation-metrics.json | 877 ++++++++++++++++++ packages/app/src/utils/webhookIcons.tsx | 8 +- packages/common-utils/src/types.ts | 3 + 29 files changed, 3247 insertions(+), 8 deletions(-) create mode 100644 .changeset/claude-managed-agents-webhook.md create mode 100644 .changeset/managed-agents-provisioning.md create mode 100644 docs/alert-webhook-claude-managed-agents.md create mode 100644 packages/api/src/models/anthropicIntegration.ts create mode 100644 packages/api/src/models/managedAgent.ts create mode 100644 packages/api/src/routers/api/managedAgents.ts create mode 100644 packages/api/src/services/__tests__/anthropicAgents.test.ts create mode 100644 packages/api/src/services/anthropicAgents.ts create mode 100644 packages/api/src/utils/__tests__/encryption.test.ts create mode 100644 packages/api/src/utils/encryption.ts create mode 100644 packages/app/src/components/TeamSettings/AgentsSection.tsx create mode 100644 packages/app/src/dashboardTemplates/api-capability-slos.json create mode 100644 packages/app/src/dashboardTemplates/api-instrumentation-metrics.json diff --git a/.changeset/claude-managed-agents-webhook.md b/.changeset/claude-managed-agents-webhook.md new file mode 100644 index 0000000000..94ad35d4af --- /dev/null +++ b/.changeset/claude-managed-agents-webhook.md @@ -0,0 +1,23 @@ +--- +'@hyperdx/common-utils': minor +'@hyperdx/api': minor +'@hyperdx/app': minor +--- + +feat: add Claude Managed Agents webhook template with enriched, agent-ready payload + +Adds a "Claude Managed Agents" webhook service type that posts an enriched, +agent-ready JSON payload (sent identically to a Generic webhook). The pre-built +body carries structured alert context (status, type, comparator, threshold, +current value, group key, source query, team id, time range) and a prompt +instructing the agent to investigate via its pre-configured ClickStack MCP +server and post a root-cause summary. No MCP URL or auth is sent in the +payload — the agent reaches ClickStack through the MCP server declared on the +agent with credentials held in a vault. + +These enriched template variables (`{{status}}`, `{{alertType}}`, +`{{comparator}}`, `{{threshold}}`, `{{value}}`, `{{groupKey}}`, +`{{sourceQuery}}`, `{{teamId}}`, `{{alertId}}`, `{{note}}`) are also available to +Generic webhook bodies for pre-agent routing and dedup. The alert's freeform +`note` is surfaced as `context.runbook` so a runbook link attached to the alert +reaches the agent. diff --git a/.changeset/managed-agents-provisioning.md b/.changeset/managed-agents-provisioning.md new file mode 100644 index 0000000000..e182ee842d --- /dev/null +++ b/.changeset/managed-agents-provisioning.md @@ -0,0 +1,16 @@ +--- +'@hyperdx/api': minor +'@hyperdx/app': minor +--- + +feat: provision Claude Managed Agents from the HyperDX UI + +Add a Managed Agents section under Team Settings → API & Agents (gated by +`HDX_MANAGED_AGENTS_ENABLED` / `NEXT_PUBLIC_HDX_MANAGED_AGENTS_ENABLED`). A team +stores its Anthropic API key (encrypted at rest via AES-256-GCM using +`HDX_ENCRYPTION_KEY`) and can provision an opinionated ClickStack SRE agent in +one click — HyperDX creates the Anthropic environment, vault (with the +provisioning user's ClickStack access key as the MCP credential), and agent with +the ClickStack MCP server pre-configured, then stores the references for +management. The Anthropic key is never returned by the API (masked, with a +4-char hint). No agent runtime/session handling is included yet. diff --git a/docs/alert-webhook-claude-managed-agents.md b/docs/alert-webhook-claude-managed-agents.md new file mode 100644 index 0000000000..5d368600cd --- /dev/null +++ b/docs/alert-webhook-claude-managed-agents.md @@ -0,0 +1,286 @@ +# Alert Webhook → Claude Managed Agents + +Drive a [Claude Managed Agent](https://platform.claude.com/docs/en/managed-agents/overview) +from a ClickStack/HyperDX alert: when an alert fires, an agent investigates your +telemetry through the ClickStack MCP server, produces a root-cause summary, posts +it to Slack, and leaves a live session your on-call engineer can pick up. + +``` +ClickStack alert ──fires──▶ Generic webhook (Claude template) + │ + ▼ + Thin receiver (verify · dedup · start session) + │ POST /v1/sessions + ▼ + Claude Managed Agent ──MCP──▶ ClickStack /api/mcp + │ session.status_idled (outbound webhook) + ▼ + Receiver fetches result ──▶ Slack + handoff link +``` + +Claude Managed Agents has **no native inbound alert trigger**, so a small +receiver bridges the HyperDX webhook to the Managed Agents API. The receiver is +operationally critical (it fires on real incidents) but small: verify, dedup, +route, start a session. + +--- + +## 1. The webhook payload HyperDX sends + +In HyperDX, create a webhook under **Team Settings → Webhooks**, choose the +**Claude Managed Agents** service type, and point the URL at your receiver. The +body is pre-filled with an enriched, agent-ready JSON payload: + +> **The payload carries no MCP URL or auth.** The agent reaches ClickStack +> through the MCP server pre-configured on the agent (§2), with the Bearer token +> held in a vault (§3) and injected by Anthropic outside the sandbox. Nothing +> secret is ever sent over the wire in the webhook. + +```json +{ + "source": "clickstack", + "schema_version": "1", + "prompt": "A ClickStack alert fired. Investigate the root cause using your pre-configured ClickStack MCP server (logs, traces, metrics, and alert history). Reconstruct and re-run the alert source_query over the time_range, inspect related logs, traces, and metrics, follow the runbook in context.runbook if present, check recent deploys, then post a structured root-cause summary to Slack and leave the session open for the on-call engineer to continue.", + "alert": { + "id": "{{alertId}}", + "event_id": "{{eventId}}", + "status": "{{status}}", + "type": "{{alertType}}", + "title": "{{title}}", + "body": "{{body}}", + "link": "{{link}}" + }, + "condition": { + "comparator": "{{comparator}}", + "threshold": "{{threshold}}", + "current_value": "{{value}}" + }, + "context": { + "group_key": "{{groupKey}}", + "source_query": "{{sourceQuery}}", + "runbook": "{{note}}", + "team_id": "{{teamId}}", + "time_range": { "start": "{{startTime}}", "end": "{{endTime}}" } + } +} +``` + +The body is a [Handlebars](https://handlebarsjs.com/) template. You can edit it +freely; the variables below are substituted at send time. + +### Template variables + +| Variable | Example | Notes | +| ----------------- | ----------------------------- | ----- | +| `{{alertId}}` | `663f…` | Stable alert id. | +| `{{eventId}}` | `a1b2…` | Per-firing dedup hash (alert + channel + group). Use as an idempotency key. | +| `{{status}}` | `firing` \| `resolved` \| `no_data` | Mapped from alert state; enables suppress-on-resolve and no-data handling. | +| `{{alertType}}` | `search` \| `dashboard_chart` | Mapped from the alert source. | +| `{{title}}` | `🚨 Alert for "5xx rate"…` | | +| `{{body}}` | rendered alert body | Includes sample log lines for saved-search alerts. | +| `{{link}}` | `https://hdx…/search/…` | Deep link into HyperDX. | +| `{{comparator}}` | `>=` `>` `<=` `<` `=` `!=` `between` `outside` | Threshold comparator. | +| `{{threshold}}` | `5` | Numeric; quoted in the default body to keep JSON valid. | +| `{{value}}` | `42` | The value that triggered/resolved the alert. | +| `{{groupKey}}` | `checkout-service` | Grouped-by value, when present. | +| `{{sourceQuery}}` | `Body: "error"` | The search expression / SQL defining the alert. JSON-escaped. | +| `{{note}}` | `Runbook: https://wiki/…` | The alert's freeform note — use it to attach a runbook link. Surfaced as `context.runbook`. | +| `{{teamId}}` | `663f…` | Owning team. | +| `{{startTime}}` / `{{endTime}}` | epoch ms | Evaluation window — scope MCP queries to it. | +| `{{state}}` | `ALERT` \| `OK` \| `INSUFFICIENT_DATA` | Raw internal state (prefer `{{status}}`). | + +> These variables are also available to the plain **Generic** webhook, so you can +> route/dedup by `service`/`severity`/`status` before spending agent tokens. + +### Security + +Add a shared-secret header in the webhook's **Headers** field and verify it in +the receiver. (A first-class signed-webhook — HMAC over the body with a per- +webhook secret + timestamp — is on the roadmap; until then, use a header secret +over HTTPS.) + +--- + +## 2. Create the agent (once) + +Declare the ClickStack MCP server at `/api/mcp`. This URL +lives only here and in the vault credential (§3) — it must be byte-identical in +both, and it is never sent in the webhook payload. + +```bash +curl -sS https://api.anthropic.com/v1/agents \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: managed-agents-2026-04-01" \ + -H "content-type: application/json" \ + -d '{ + "name": "ClickStack SRE Responder", + "model": "claude-opus-4-8", + "system": "You are an SRE agent. A ClickStack alert fired. Investigate via the clickstack MCP server (logs/traces/metrics + alert history), reconstruct and re-run the alert source_query over the time_range, check recent deploys, then post a structured root-cause summary to Slack.", + "mcp_servers": [ + { "type": "url", "name": "clickstack", "url": "https:///api/mcp" } + ], + "tools": [ + { "type": "agent_toolset_20260401" }, + { "type": "mcp_toolset", "mcp_server_name": "clickstack" } + ] + }' +``` + +Save the returned `agent_id`. + +**Tool permissions** default to `always_ask`. For an unattended loop, auto-allow +the read tools via `default_config`/`configs` on the `mcp_toolset`, and keep +write/remediation tools disabled until precision is validated (read-only first). +See [permission policies](https://platform.claude.com/docs/en/managed-agents/permission-policies). + +Create an environment once and save its `environment_id`: + +```bash +curl -sS https://api.anthropic.com/v1/environments \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: managed-agents-2026-04-01" \ + -H "content-type: application/json" \ + -d '{ "name": "sre-sandbox", "config": { "type": "cloud", "networking": { "type": "unrestricted" } } }' +``` + +--- + +## 3. Store secrets in a vault (once) + +The model never sees the token — Anthropic's credential proxy injects it by +matching `mcp_server_url` to the agent's MCP server `url`. **Same exact URL.** + +```bash +# create the vault → save vault_id +curl -sS https://api.anthropic.com/v1/vaults \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: managed-agents-2026-04-01" \ + -H "content-type: application/json" \ + -d '{ "display_name": "ClickStack SRE Credentials" }' + +# add the ClickStack Personal API Access Key (Team Settings → API Keys) +curl -sS https://api.anthropic.com/v1/vaults/$VAULT_ID/credentials \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: managed-agents-2026-04-01" \ + -H "content-type: application/json" \ + -d '{ + "display_name": "ClickStack Personal API Access Key", + "auth": { + "type": "static_bearer", + "mcp_server_url": "https:///api/mcp", + "token": "" + } + }' +``` + +Add Slack / GitHub credentials to the same vault as needed (least privilege: +read-only ClickStack, Slack write scoped to one channel). + +--- + +## 4. The receiver (per alert) + +A small HTTPS service: verify the shared-secret header, dedup on +`alert.event_id` within the firing window, then start a session and hand it the +payload as the prompt. + +```js +// 1. verify header secret, 2. dedup on payload.alert.event_id, then: +const headers = { + 'x-api-key': process.env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'managed-agents-2026-04-01', + 'content-type': 'application/json', +}; + +const session = await fetch('https://api.anthropic.com/v1/sessions', { + method: 'POST', headers, + body: JSON.stringify({ + agent: AGENT_ID, + environment_id: ENV_ID, + vault_ids: [VAULT_ID], + title: payload.alert.title, + }), +}).then(r => r.json()); + +await fetch(`https://api.anthropic.com/v1/sessions/${session.id}/events`, { + method: 'POST', headers, + body: JSON.stringify({ + events: [{ type: 'user.message', content: [{ type: 'text', text: JSON.stringify(payload) }] }], + }), +}); +``` + +The enriched body is already agent-ready — `condition` and `context` tell the +agent what to query without a round-trip. It reaches ClickStack through its +pre-configured MCP server, so the payload never carries an MCP URL or token. + +--- + +## 5. Return the result to Slack + +Register an outbound webhook in the Claude console (**Manage → Webhooks**) for +`session.status_idled`. On receipt, verify the signature (HMAC-SHA256 over the +raw body) and fetch the result: + +```python +import anthropic +client = anthropic.Anthropic() # reads ANTHROPIC_WEBHOOK_SIGNING_KEY + +event = client.beta.webhooks.unwrap(request_body, headers) # raises on bad sig / >5 min old +if event.data.type == "session.status_idled": + events = client.beta.sessions.events.list(event.data.id) + summary = next((e.content[0].text for e in events if e.type == "agent.message"), "") + # post `summary` to Slack; include a deep link to the session for handoff +``` + +### Suggested Slack format + +``` + () +Fired: