Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
376663c
Build evidence-first investigation workspace
nadaverell Sep 2, 2026
9f33065
Link investigation findings to verified evidence
nadaverell Sep 2, 2026
f079be7
Refine investigation evidence hierarchy and provenance
nadaverell Sep 3, 2026
16182c8
Use themed tooltips for evidence timestamps
nadaverell Sep 3, 2026
05c828e
Refine investigation evidence navigation and activity density
nadaverell Sep 3, 2026
15bc495
Harden investigation evidence integrity and navigation
nadaverell Sep 3, 2026
194880d
Refine investigation findings around operator evidence
nadaverell Sep 3, 2026
95d99dc
Simplify Findings visual hierarchy and evidence navigation
nadaverell Sep 6, 2026
5bc76ba
Group Findings into clear sections and compact supporting evidence
nadaverell Sep 6, 2026
1ee2a9a
Make investigation history a compact resource-first navigation list
nadaverell Sep 6, 2026
d088a75
Refine investigation navigation and add on-demand assessment explanat…
nadaverell Sep 6, 2026
894ff7f
Merge main into investigation Findings branch
nadaverell Sep 6, 2026
6de4072
Fix investigation reveal effects and PostgreSQL evidence filtering
nadaverell Sep 6, 2026
9fc5778
Use non-blocking themed tooltips in investigation history
nadaverell Sep 6, 2026
d1f406a
Remove unused investigation card and redundant changes wrapper
nadaverell Sep 6, 2026
474a948
Remove unreachable recent-change identity fallbacks
nadaverell Sep 6, 2026
8547234
Tighten investigation lifecycle and separate mutation outcomes
nadaverell Sep 6, 2026
ce957a8
Compare resource revisions independently of tool context
nadaverell Sep 6, 2026
27a6e0c
Separate investigation presentation rules and reuse shared helpers
nadaverell Sep 6, 2026
c5cb6fc
Preserve investigation context and clarify evidence and proposed actions
nadaverell Sep 7, 2026
a23f07c
Refine investigation evidence relevance and presentation
nadaverell Sep 7, 2026
ca6fcb1
Avoid unused bindings in evidence presentation normalization
nadaverell Sep 7, 2026
8ac5555
Accept upgrade readiness results as investigation evidence
nadaverell Sep 7, 2026
c9d3873
Document Cloud investigation integration contract
nadaverell Sep 7, 2026
9764530
Revert "Document Cloud investigation integration contract"
nadaverell Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ curl -fsSL https://get.radarhq.io | sh && kubectl radar
- **Airgapped-friendly** — runs as a single binary against the Kubernetes API and works in locked-down environments with outbound egress blocked
- **Real-time** — watches your cluster via informers, pushes updates to the browser via SSE
- **Works everywhere** — GKE, EKS, AKS, minikube, kind, k3s, or any conformant cluster
- **AI-ready** — built-in [MCP server](docs/mcp.md) lets AI agents inspect, diagnose, and operate your cluster through Radar
- **AI-ready** — built-in [MCP server](docs/mcp.md) lets AI agents inspect, investigate, and operate your cluster through Radar
- **In-cluster option** — deploy with Helm for shared team access with RBAC-scoped permissions

> "Have Radar deployed at work. As far as Kubernetes dashboards go, this is one of the best." — u/TheRealNetroxen
Expand Down Expand Up @@ -465,7 +465,7 @@ Read-only visibility ships first; the considered follow-ups (RBAC audit checks,

### AI Integration (MCP)

Radar includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI agents — Claude, Cursor, Copilot, and others — inspect, diagnose, and operate your cluster through Radar.
Radar includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI agents — Claude, Cursor, Copilot, and others — inspect, investigate, and operate your cluster through Radar.

Instead of raw `kubectl` output (verbose YAML that burns through LLM context windows), your AI gets pre-processed, token-optimized data: topology graphs, health assessments, deduplicated events, and filtered logs. Diagnosis is read-only by default; optional in-cluster route probing uses short-lived, self-deleting probe pods. Write operations such as restart, scale, apply, and rollback are identified for client confirmation and enforced through Kubernetes RBAC.

Expand Down
4 changes: 2 additions & 2 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ func main() {
timelineDBPath := flag.String("timeline-db", fileCfg.TimelineDBPath, "Path to timeline database file (default: ~/.radar/timeline.db)")
timelineRetention := flag.Duration("timeline-retention", fileCfg.TimelineRetentionOr(7*24*time.Hour), "How long to retain timeline events when --timeline-storage=sqlite or postgres (e.g. 168h, 720h). 0 disables age-based cleanup.")
timelineMaxSize := flag.String("timeline-max-size", fileCfg.TimelineMaxSizeOr("1Gi"), "Maximum SQLite timeline storage size before pruning oldest events (e.g. 800Mi, 8Gi). 0 disables size-based pruning.")
// AI history (Diagnose investigations)
aiHistory := flag.Bool("ai-history", fileCfg.AIHistoryOr(true), "Persist AI investigations (transcripts + verdicts) to ~/.radar/ai-runs.db so they survive restarts")
// AI investigation history
aiHistory := flag.Bool("ai-history", fileCfg.AIHistoryOr(true), "Persist AI investigations (transcripts + conclusions) to ~/.radar/ai-runs.db so they survive restarts")
// Traffic/metrics options
prometheusURL := flag.String("prometheus-url", fileCfg.PrometheusURL, "Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)")
openCostCurrency := flag.String("opencost-currency", fileCfg.OpenCostCurrency, "Override the ISO 4217 currency label for OpenCost values (empty: auto-detect, then USD)")
Expand Down
24 changes: 22 additions & 2 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AI Integration (MCP)

Radar includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI agents inspect, diagnose, and operate your Kubernetes cluster.
Radar includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI agents inspect, investigate, and operate your Kubernetes cluster.

## Why MCP instead of raw kubectl?

Expand Down Expand Up @@ -195,14 +195,26 @@ docker run -p 127.0.0.1:9280:9280 \

The Docker image's primary use is [in-cluster deployment](in-cluster.md) with a ServiceAccount, where none of these apply.

## Diagnose evidence limits

For workload `diagnose` responses, `logCoverage.selectedPods` counts pods selected for log requests, not pods whose logs were successfully read. Per-container errors and `logsError` describe failed collection. `logCoverage.totalLines` counts diagnostic-filter output before the aggregate response cap (including fallback tail lines); `shownLines` counts the lines retained after that cap. Neither describes a container's complete log history. `totalPods` and `shownPods` count pods contributing at least one line before and after that cap—not all selected pods or successful empty reads. `eventsTotalGroups`, recent-change coverage/error fields, and log sampling/truncation must remain qualifications even when an agent reports no problem.

`expectedPreviousLogAbsences` requires container status consistent with no previous instance and either an empty successful read or the specific previous-instance-not-found response. Permission failures, missing pods, unavailable servers, and interrupted reads remain collection errors.

One call combines cached Kubernetes resource state, historical events/changes, and live log requests. Its completion time records when the call finished, not a shared observation time or freshness guarantee for those sources. Event/change timestamps retain their source-specific meaning.

These public MCP fields are available to consumers running the updated Radar server. The OSS local AI investigation additionally uses its own evidence ledger, citations, and Findings integration. Hosted/Cloud consumers need corresponding backend provenance and investigation integration; upgrading a frontend package alone does not establish parity.

## Available Tools

### Read Tools

For interpreting workload bundles, see [Diagnose evidence limits](#diagnose-evidence-limits), including collection failures, sampling, source timestamps, and the public MCP versus local investigation boundary.

| Tool | Description | Parameters |
|------|-------------|------------|
| `issues` | "What's broken right now?" — a ranked, curated stream of live operational failures: failing workloads/pods, active native Helm release failures or stuck pending operations (`kind=HelmRelease`, `group=helm.sh`), dangling references, pod-startup blockers (unschedulable / admission-rejected / stuck post-bind), and False CRD conditions. No source filter; each row carries a `source` label sliceable via `filter`. Recovered Helm rollbacks are deploy history, not live issues; use `get_changes` for Helm deployment history and `get_helm_release` for native Helm full revision/history/hook diagnostics. Flux `HelmRelease` rows (`group=helm.toolkit.fluxcd.io`) are GitOps reconcilers and should use `diagnose`. For static posture use `get_cluster_audit`; for raw events use `get_events`. | `namespace` (optional), `severity` (optional: `critical,warning`), `kind` (optional), `filter` (optional CEL), `limit` (optional, default 200, max 1000) |
| `diagnose` | Root-cause one workload, GitOps reconciler, or network entry kind in a single call. Pod/Deployment/StatefulSet/DaemonSet get minified resource + `resourceContext` + current AND previous container logs across pods + `crashCause` evidence that pairs active crashloop status with one highest-signal filtered log line (`logLineSelection` states how the line was chosen, in descending confidence: `fatal_pattern`, `traceback_header_only`, `last_matched_line`, `log_tail`) + filtered events + `startupBlockers`; Application/Kustomization/Flux HelmRelease get reconciler status + related parsed issues; Service/Ingress/HTTPRoute/GRPCRoute/Gateway get a coverage-honest [reachability diagnosis](reachability.md): a `summary` (headline + tested/passed/failed/skipped counts over the intended routes), `routes` (per-route outcome + `confidence` - `indirect` = reached only via the API-server proxy, not the live-traffic path), `notTested` (with why + a copyable command), a named `brokenRoute`, and `path` (hops with static findings). Optional active reachability test (`probe: true`, bounded to a 3s total budget; a timeout returns an honest partial). In `resourceContext`, `issueSummary` uses live-operational `critical`/`warning`, while `auditSummary` uses the Checks posture-remediation ladder `critical`/`high`/`medium`/`low` (current built-ins are `high`/`medium`) and is not evidence of an active outage. | `kind` (required: workload, GitOps reconciler, or network entry kind), `namespace` (required), `name` (required), `probe` (optional: network kinds only - run DNS/TCP/TLS/HTTP probes as applicable; explicitly non-HTTP Service ports stop at TCP), `in_cluster` (optional on the full `/mcp` endpoint only, **MUTATING** - network kinds only: runs the applicable probe layers from inside the cluster by creating up to 5 short-lived, self-deleting probe pods under your RBAC; the only diagnose option with a side effect, requires `create jobs`, `list pods`, and `get pods/log`; forces `probe` on; omitted from and rejected by `/mcp-readonly`) |
| `diagnose` | Collect a bounded, point-in-time evidence bundle for one narrowed workload, GitOps reconciler, or network entry kind in a single call. It supports diagnosis but does not run an agent, provide an authoritative root-cause verdict, or exhaustively read every source. Pod/Deployment/StatefulSet/DaemonSet and Argo Rollout (`kind=Rollout`, `group=argoproj.io`) get minified resource + `resourceContext` + selected, capped current and previous container logs across pods + `crashCause` evidence that pairs active crashloop status with one highest-signal filtered log line (`logLineSelection` states how the line was chosen, in descending confidence: `fatal_pattern`, `traceback_header_only`, `last_matched_line`, `log_tail`) + filtered events + `startupBlockers`; Application/Kustomization/Flux HelmRelease get reconciler status + related parsed issues; Service/Ingress/HTTPRoute/GRPCRoute/Gateway get coverage-honest [reachability evidence](reachability.md): a `summary` (headline + tested/passed/failed/skipped counts over the intended routes), `routes` (per-route outcome + `confidence` - `indirect` = reached only via the API-server proxy, not the live-traffic path), `notTested` (with why + a copyable command), an optional named `brokenRoute` when established, and `path` (hops with static findings). Optional active reachability test (`probe: true`, bounded to a 3s total budget; a timeout returns an honest partial). The returned bundle is frozen from one call, but its sources are not read atomically; preserve its limits, errors, timestamps, and confidence. In `resourceContext`, `issueSummary` uses live-operational `critical`/`warning`, while `auditSummary` uses the Checks posture-remediation ladder `critical`/`high`/`medium`/`low` (current built-ins are `high`/`medium`) and is not evidence of an active outage. | `kind` (required: workload, GitOps reconciler, or network entry kind), `group` (optional API group; set it for CRDs or same-kind collisions, for example `argoproj.io` for `Rollout`; built-in groups are inferred when omitted), `namespace` (required), `name` (required), `probe` (optional: network kinds only - run DNS/TCP/TLS/HTTP probes as applicable; explicitly non-HTTP Service ports stop at TCP), `in_cluster` (optional on the full `/mcp` endpoint only, **MUTATING** - network kinds only: runs the applicable probe layers from inside the cluster by creating up to 5 short-lived, self-deleting probe pods under your RBAC; the only diagnose option with a side effect, requires `create jobs`, `list pods`, and `get pods/log`; forces `probe` on; omitted from and rejected by `/mcp-readonly`) |
| `get_dashboard` | Cluster/namespace health overview — resource counts, failing pods, unhealthy workloads, warning-event groups (`warningGroups`, up to 20 recency-ordered; `totalWarningGroups`/`warningGroupsTruncated` signal when more exist), Helm status. Inventory-style triage before drilling in. | `namespace` (optional) |
| `top_resources` | Live metrics ranked like `kubectl top | sort`, joined with K8s context (status, restarts, owner, requests/limits). Use for CPU/memory/OOM/load symptoms. | `kind` (optional: `pods` default, `workloads`, `nodes`), `namespace` (optional), `sort` (optional: `cpu` default, `memory`), `limit` (optional, default 20, max 100) |
| `list_resources` | List resources of a kind with minified summaries + per-row `summaryContext` (managedBy / health / issueCount). | `kind` (required), `group` (optional), `namespace` (optional), `context` (optional: default / `none`) |
Expand All @@ -225,6 +237,14 @@ The Docker image's primary use is [in-cluster deployment](in-cluster.md) with a
| `discover_metrics` | Discover exact metric names (enriched with type/help from Prometheus metadata) or values of one label before writing PromQL. Lists active series from the last hour; `truncated: true` means narrow the `match` selector. | `match` (PromQL series selector; required when `label` is empty), `label` (optional: list values of this label instead of metric names), `limit` (optional, default 100, max 500) |
| `get_prometheus_rules` | List Prometheus alerting/recording rules with PromQL definitions, state, labels, annotations, and active alert instances. Alert-investigation entry point: fetch the rule definition, then run its query with `query_prometheus`. | `type` (optional: `alert`, `record`), `name` / `group` (optional substring filters), `state` (optional: `firing`, `pending`, `inactive`), `limit` (optional, default 50, max 200) |

API group is part of a resource's identity. Radar infers the canonical group for built-in kinds, but callers should pass `group` for a supported CRD or whenever a Kind can exist in more than one group. For example, diagnose an Argo Rollout with:

```json
{"kind":"Rollout","group":"argoproj.io","namespace":"prod","name":"checkout"}
```

This selects the Argo Rollout workload path; it does not imply that `diagnose` supports arbitrary custom resource shapes.

For `issues`, read `timing_summary` when present; it explains timing combinations that are easy to misread without schema context. The raw provenance fields remain available for filtering. `first_seen` is an evidence-backed lower bound, `onset_unknown` means no contributing signal has a known onset, and `resource_created_at` is resource-age context rather than issue age. A missing `first_seen` is exposed to CEL as `0`; require `first_seen != 0` for any age filter, and also require `onset_coverage_unknown == 0` when the whole row must have exact timing.

### Write Tools
Expand Down
6 changes: 3 additions & 3 deletions docs/reachability.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ The **Reachability** tab in the resource detail view answers one question for a

Available in Radar **v1.9.1+** (the in-cluster probe test also requires the probe image from v1.9.1+).

Naming note: this is NOT the [AI Diagnose feature](https://radarhq.io/docs/features/diagnose) - Reachability is deterministic path tracing and live probing, no AI involved. The general-purpose MCP `diagnose` tool (whose primary job is workload root-cause bundles: logs, events, crash evidence) returns this reachability trace as its answer when pointed at a network entry kind - see [MCP](#mcp).
Naming note: this is NOT the [AI Investigations feature](https://radarhq.io/docs/features/diagnose) - Reachability is deterministic path tracing and live probing, no AI involved. The general-purpose MCP `diagnose` tool returns a bounded, point-in-time evidence bundle for a narrowed workload or reconciler, and returns this reachability trace when pointed at a network entry kind - see [MCP](#mcp).

The trace has two layers:

1. **Static** - is the path wired correctly in config + current pod state? Pure functions over the in-memory informer cache, no per-call API requests. Always on.
2. **Active reachability test** (optional, one-shot) - send DNS / TCP / TLS / HTTP probes along the declared path and report what came back. HTTP-shaped ports get an HTTP request; explicitly non-HTTP ports stop at TCP rather than sending an unrelated protocol. The proxy probe runs automatically once when the **Diagnose** tab opens (re-runnable via **Run test**); only the in-cluster Job test stays a manual click.
2. **Active reachability test** (optional, one-shot) - send DNS / TCP / TLS / HTTP probes along the declared path and report what came back. HTTP-shaped ports get an HTTP request; explicitly non-HTTP ports stop at TCP rather than sending an unrelated protocol. The proxy probe runs automatically once when the **Reachability** tab opens (re-runnable via **Run test**); only the in-cluster Job test stays a manual click.

The active layer can escalate the static verdict when probes give clear evidence of a real failure on a hop (every non-skipped probe failed → that hop counts toward broken; over half failed → counts toward degraded). It never softens a static verdict: a critical static finding outranks probe state, and an unverifiable path stays unverifiable.

Expand Down Expand Up @@ -176,7 +176,7 @@ The UI shows the verdict at the top of the panel with a one-sentence reason. Tre

## MCP

The general-purpose `diagnose` MCP tool - primarily a workload root-cause tool (logs, Warning events, crash evidence in one call) - returns the reachability trace for network entry kinds instead of the pod-log fan-out it does for workloads. An agent that calls `diagnose(kind=service, ...)` gets the path-shaped answer in one call, along with `relatedIssues` for raw-issue follow-up. Pass `probe: true` to add the active reachability test from Radar's vantage. Pass `in_cluster: true` to run the probe from inside the cluster - Radar creates up to 5 short-lived, self-destructing probe pods (one per intended route) under the active Kubernetes identity's RBAC to test the real dataplane the API-server-proxy vantage can't reach (e.g. to confirm a route that came back `indirect`). This is the only mutating `diagnose` option; it needs `create jobs`, `list pods`, and `get pods/log`, and falls back to a copyable command when any permission is missing.
The general-purpose `diagnose` MCP tool collects a bounded, point-in-time evidence bundle for a narrowed target; it does not run an agent or promise a root-cause verdict. For network entry kinds it returns this reachability trace instead of the pod-log fan-out used for workloads. An agent that calls `diagnose(kind=service, ...)` gets the path-shaped answer in one call, along with `relatedIssues` for raw-issue follow-up. Pass `probe: true` to add the active reachability test from Radar's vantage. Pass `in_cluster: true` to run the probe from inside the cluster - Radar creates up to 5 short-lived, self-destructing probe pods (one per intended route) under the active Kubernetes identity's RBAC to test the real dataplane the API-server-proxy vantage can't reach (e.g. to confirm a route that came back `indirect`). This is the only mutating `diagnose` option; it needs `create jobs`, `list pods`, and `get pods/log`, and falls back to a copyable command when any permission is missing.

## In-cluster probe image

Expand Down
2 changes: 1 addition & 1 deletion internal/ai/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
)

// Agent abstracts a coding CLI radar drives for AI diagnosis. Each backend knows
// Agent abstracts a coding CLI Radar drives for AI investigations. Each backend knows
// how to spawn its CLI for one turn (flags, MCP wiring, env, cwd) and how to parse
// that CLI's event stream into radar's normalized StreamEvents + final Diagnosis.
// The generic run loop (process group, stdout pipe, lifecycle) lives in Diagnoser.
Expand Down
21 changes: 21 additions & 0 deletions internal/ai/agent_claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,24 @@ func TestClaudeExecutionProfiles(t *testing.T) {
t.Fatal("Claude must reject an empty profile rather than fail open")
}
}

func TestClaudeSafeguardedApplyAllowsEveryRadarWriteTool(t *testing.T) {
a := &claudeAgent{bin: "claude"}
cmd, cleanup, err := a.command(context.Background(), turnSpec{
mcpURL: "http://localhost:1/mcp", prompt: "apply",
profile: ExecutionProfileSafeguarded, apply: true, maxTurns: 1,
})
if err != nil {
t.Fatal(err)
}
defer cleanup()
args := strings.Join(cmd.Args, " ")
for _, tool := range radarWriteTools {
if !strings.Contains(args, "mcp__radar__"+tool) {
t.Errorf("safeguarded apply command missing write tool %q: %q", tool, args)
}
}
if !strings.Contains(args, "mcp__radar__manage_rollout") {
t.Errorf("real Rollout mutations must be available on apply turns: %q", args)
}
}
Loading
Loading