@skyhook-io/radar-app 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +7 -7
  2. package/src/App.tsx +69 -16
  3. package/src/api/apiResources.test.ts +11 -0
  4. package/src/api/apiResources.ts +51 -12
  5. package/src/api/client.capacity.test.ts +92 -0
  6. package/src/api/client.ts +2905 -2081
  7. package/src/api/config.test.ts +47 -0
  8. package/src/api/config.ts +15 -0
  9. package/src/api/diagnose.ts +15 -15
  10. package/src/components/ConnectionErrorView.test.tsx +88 -0
  11. package/src/components/ConnectionErrorView.tsx +128 -22
  12. package/src/components/capacity/CapacityActivity.tsx +787 -0
  13. package/src/components/capacity/CapacityDemand.tsx +961 -0
  14. package/src/components/capacity/CapacityOverview.tsx +1529 -0
  15. package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
  16. package/src/components/capacity/CapacityView.test.tsx +2287 -0
  17. package/src/components/capacity/CapacityView.tsx +85 -0
  18. package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
  19. package/src/components/capacity/DemandNomination.test.tsx +151 -0
  20. package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
  21. package/src/components/capacity/coverageCertainty.test.ts +162 -0
  22. package/src/components/capacity/podDemandGate.test.ts +47 -0
  23. package/src/components/capacity/podDemandGate.ts +22 -0
  24. package/src/components/capacity/schedulingBar.test.ts +244 -0
  25. package/src/components/capacity/shared.tsx +1841 -0
  26. package/src/components/diagnose/AISettings.tsx +21 -7
  27. package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
  28. package/src/components/diagnose/DiagnoseContext.tsx +127 -57
  29. package/src/components/diagnose/DiagnoseSurface.tsx +33 -15
  30. package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
  31. package/src/components/diagnose/agentCatalog.ts +30 -0
  32. package/src/components/diagnose/parts.test.tsx +125 -0
  33. package/src/components/diagnose/parts.tsx +166 -75
  34. package/src/components/home/CapacityCard.test.tsx +150 -0
  35. package/src/components/home/CapacityCard.tsx +125 -0
  36. package/src/components/home/HomeView.tsx +15 -1
  37. package/src/components/issues/IssuesPane.test.ts +142 -0
  38. package/src/components/issues/IssuesPane.tsx +142 -38
  39. package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
  40. package/src/components/nav/PrimaryNavRail.tsx +191 -103
  41. package/src/components/resources/ResourcesView.tsx +9 -8
  42. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
  43. package/src/components/resources/renderers/PodRenderer.tsx +32 -3
  44. package/src/components/settings/SettingsDialog.tsx +31 -19
  45. package/src/components/timeline/TimelineView.tsx +17 -3
  46. package/src/components/ui/command-items.ts +222 -98
  47. package/src/components/workload/WorkloadView.tsx +16 -83
  48. package/src/context/ConnectionContext.test.ts +39 -0
  49. package/src/context/ConnectionContext.tsx +155 -51
  50. package/src/context/DiagnoseCustomization.tsx +1 -1
  51. package/src/utils/shell-safe.test.ts +55 -0
  52. package/src/utils/shell-safe.ts +21 -0
@@ -1,5 +1,9 @@
1
1
  import { Loader2, Sparkles } from "lucide-react";
2
- import { useDiagnose, useDiagnoseLayout, runTargetKey } from "./DiagnoseContext";
2
+ import {
3
+ useDiagnose,
4
+ useDiagnoseLayout,
5
+ runTargetKey,
6
+ } from "./DiagnoseContext";
3
7
  import { Tooltip } from "../ui/Tooltip";
4
8
  import type { RenderDiagnoseAction } from "../../context/DiagnoseCustomization";
5
9
 
@@ -27,31 +31,41 @@ function DiagnoseResourceButton({
27
31
  }) {
28
32
  const d = useDiagnose();
29
33
  const { runningKeys } = useDiagnoseLayout();
30
- if (!d.available) return null;
34
+ // Hidden only when the feature can't work here (auth/cloud/--no-mcp). When it's
35
+ // supported but no agent is installed we KEEP the button — clicking opens the
36
+ // setup notice so the feature is discoverable rather than silently absent.
37
+ if (d.setupState === "off") return null;
38
+ const ready = d.available;
31
39
  const problem = health === "problem";
32
- const running = runningKeys.has(runTargetKey(kind, namespace, name));
33
- const tooltip = running
34
- ? `${d.agentLabel} is investigating this resource click to watch it live.`
35
- : d.hosted
36
- ? problem
37
- ? `Diagnose with ${d.agentLabel} — reads this resource's spec, events & logs to find the root cause.`
38
- : `Ask ${d.agentLabel} about this resource — reads its spec, events & logs.`
39
- : problem
40
- ? `Diagnose with your own ${d.agentLabel} runs locally, reads this resource's spec, events & logs to find the root cause.`
41
- : `Ask your own ${d.agentLabel} about this resource — runs locally, reads its spec, events & logs.`;
40
+ const running = ready && runningKeys.has(runTargetKey(kind, namespace, name));
41
+ const tooltip = !ready
42
+ ? "Set up AI diagnosis to investigate this — runs your own agent locally."
43
+ : running
44
+ ? `${d.agentLabel} is investigating this resource — click to watch it live.`
45
+ : d.hosted
46
+ ? problem
47
+ ? `Diagnose with ${d.agentLabel} — reads this resource's spec, events & logs to find the root cause.`
48
+ : `Ask ${d.agentLabel} about this resource reads its spec, events & logs.`
49
+ : problem
50
+ ? `Diagnose with your own ${d.agentLabel} — runs locally, reads this resource's spec, events & logs to find the root cause.`
51
+ : `Ask your own ${d.agentLabel} about this resource — runs locally, reads its spec, events & logs.`;
42
52
  // While an investigation is live, the button advertises it (and clicking focuses
43
53
  // the existing run rather than starting a new one — openInvestigation dedups).
44
54
  const showLabel = problem || running;
45
55
  return (
46
56
  <Tooltip content={tooltip} position="bottom">
47
57
  <button
48
- onClick={() => d.openInvestigation({ kind, namespace, name })}
58
+ onClick={() =>
59
+ ready ? d.openInvestigation({ kind, namespace, name }) : d.openHome()
60
+ }
49
61
  aria-label={
50
- running
51
- ? "Investigation running click to view"
52
- : problem
53
- ? "Diagnose with AI"
54
- : "Ask AI about this resource"
62
+ !ready
63
+ ? "Set up AI diagnosis"
64
+ : running
65
+ ? "Investigation running — click to view"
66
+ : problem
67
+ ? "Diagnose with AI"
68
+ : "Ask AI about this resource"
55
69
  }
56
70
  className={
57
71
  showLabel
@@ -97,20 +111,26 @@ export function IssueDiagnoseButton({
97
111
  name: string;
98
112
  }) {
99
113
  const d = useDiagnose();
100
- if (!d.available) return null;
114
+ if (d.setupState === "off") return null;
115
+ const ready = d.available;
101
116
  return (
102
117
  <Tooltip
103
118
  content={
104
- d.hosted
105
- ? `Sends this resource's context to ${d.agentLabel} to find the root cause`
106
- : `Runs ${d.agentLabel} on your machine and sends it this resource's context to find the root cause`
119
+ d.setupState === "needs-restart"
120
+ ? "Restart Radar to enable AI diagnosis a supported agent is installed"
121
+ : !ready
122
+ ? "Set up AI diagnosis — install a local agent to investigate"
123
+ : d.hosted
124
+ ? `Sends this resource's context to ${d.agentLabel} to find the root cause`
125
+ : `Runs ${d.agentLabel} on your machine and sends it this resource's context to find the root cause`
107
126
  }
108
127
  position="left"
109
128
  >
110
129
  <button
111
130
  onClick={(e) => {
112
131
  e.stopPropagation();
113
- d.openInvestigation({ kind, namespace, name });
132
+ if (ready) d.openInvestigation({ kind, namespace, name });
133
+ else d.openHome();
114
134
  }}
115
135
  className="flex shrink-0 items-center gap-1 rounded-md border border-theme-border px-2 py-1 text-xs text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
116
136
  >
@@ -126,7 +146,8 @@ export function IssueDiagnoseButton({
126
146
  export function GlobalDiagnoseButton() {
127
147
  const d = useDiagnose();
128
148
  const { runningKeys } = useDiagnoseLayout();
129
- if (!d.available) return null;
149
+ if (d.setupState === "off") return null;
150
+ const ready = d.available;
130
151
  const runningCount = runningKeys.size;
131
152
  const agentSuffix = d.hosted
132
153
  ? `powered by ${d.agentLabel}`
@@ -134,9 +155,11 @@ export function GlobalDiagnoseButton() {
134
155
  return (
135
156
  <Tooltip
136
157
  content={
137
- runningCount > 0
138
- ? `${runningCount} investigation${runningCount > 1 ? "s" : ""} running — ${agentSuffix}`
139
- : `AI investigations — ${agentSuffix}`
158
+ !ready
159
+ ? "Set up AI investigations runs your own agent locally"
160
+ : runningCount > 0
161
+ ? `${runningCount} investigation${runningCount > 1 ? "s" : ""} running — ${agentSuffix}`
162
+ : `AI investigations — ${agentSuffix}`
140
163
  }
141
164
  position="bottom"
142
165
  >
@@ -0,0 +1,30 @@
1
+ // Install metadata for the agent CLIs Radar can drive. Used by the setup notice
2
+ // shown when AI diagnosis is eligible but no agent is installed. Names match the
3
+ // backend's supported set (see internal/ai/detect.go knownAgents + isSupportedAgent).
4
+ export interface AgentInstall {
5
+ name: string; // stable id, matches AgentInfo.name
6
+ label: string; // display name
7
+ install: string; // one-line install command
8
+ docs: string; // canonical setup/docs URL
9
+ }
10
+
11
+ export const SUPPORTED_AGENTS: AgentInstall[] = [
12
+ {
13
+ name: "claude",
14
+ label: "Claude Code",
15
+ install: "npm install -g @anthropic-ai/claude-code",
16
+ docs: "https://docs.claude.com/en/docs/claude-code/setup",
17
+ },
18
+ {
19
+ name: "codex",
20
+ label: "Codex",
21
+ install: "npm install -g @openai/codex",
22
+ docs: "https://github.com/openai/codex",
23
+ },
24
+ {
25
+ name: "cursor-agent",
26
+ label: "Cursor CLI",
27
+ install: "curl https://cursor.com/install -fsS | bash",
28
+ docs: "https://docs.cursor.com/en/cli/overview",
29
+ },
30
+ ];
@@ -0,0 +1,125 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { AgentControls, ConsentCard } from "./parts";
4
+ import type { AgentInfo, ExecutionProfile } from "../../api/diagnose";
5
+
6
+ const noop = vi.fn();
7
+
8
+ function renderAgent(
9
+ agent: string,
10
+ profiles: ExecutionProfile[],
11
+ profile: ExecutionProfile,
12
+ ) {
13
+ const agents: AgentInfo[] = [
14
+ {
15
+ name: agent,
16
+ label: agent,
17
+ path: agent,
18
+ version: "",
19
+ present: true,
20
+ supported: true,
21
+ profiles,
22
+ },
23
+ ];
24
+ return renderToStaticMarkup(
25
+ <AgentControls
26
+ agents={agents}
27
+ selectedAgent={agent}
28
+ onSelectAgent={noop}
29
+ profile={profile}
30
+ onSetProfile={noop}
31
+ model=""
32
+ onSetModel={noop}
33
+ effort=""
34
+ onSetEffort={noop}
35
+ />,
36
+ );
37
+ }
38
+
39
+ describe("AgentControls execution profile explanation", () => {
40
+ it("shows Cursor's fixed full-local warning even when the stored profile is stale", () => {
41
+ const html = renderAgent("cursor-agent", ["full-local"], "safeguarded");
42
+ expect(html).toContain("must use this agent");
43
+ expect(html).toContain("normal setup");
44
+ expect(html).toContain("always loads your global MCP servers");
45
+ expect(html).toContain("still enables the agent CLI");
46
+ expect(html).toContain("does not constrain external MCP servers");
47
+ expect(html).not.toContain("always runs this agent with safeguards");
48
+ });
49
+
50
+ it("discloses the Claude configuration that Radar cannot exclude", () => {
51
+ const html = renderAgent(
52
+ "claude",
53
+ ["safeguarded", "full-local"],
54
+ "safeguarded",
55
+ );
56
+ expect(html).toContain("built-in tools are disabled");
57
+ expect(html).toContain("settings, hooks, and CLAUDE.md instructions still apply");
58
+ expect(html).toContain("Your claude setup");
59
+ expect(html).not.toContain("other agent configuration is excluded");
60
+ });
61
+
62
+ it("explains that Claude full-local inherits the user's permissions", () => {
63
+ const html = renderAgent(
64
+ "claude",
65
+ ["safeguarded", "full-local"],
66
+ "full-local",
67
+ );
68
+ expect(html).toContain("permissions from your setup");
69
+ expect(html).toContain("Radar does not override them");
70
+ expect(html).not.toContain("Radar still enables the agent CLI");
71
+ });
72
+
73
+ it("does not describe an unknown safeguarded agent as Codex", () => {
74
+ const html = renderAgent("future-agent", ["safeguarded"], "safeguarded");
75
+ expect(html).toContain("documented restrictions");
76
+ expect(html).not.toContain("Codex");
77
+ expect(html).not.toContain("built-in tools are disabled");
78
+ });
79
+
80
+ it("labels the selectable full-local profile as the user's agent setup", () => {
81
+ const html = renderAgent(
82
+ "codex",
83
+ ["safeguarded", "full-local"],
84
+ "safeguarded",
85
+ );
86
+ expect(html).toContain("Your codex setup");
87
+ expect(html).not.toContain("Full local setup");
88
+ });
89
+ });
90
+
91
+ describe("ConsentCard execution profile treatment", () => {
92
+ it("uses warning chrome and explicit consequences for the user's agent setup", () => {
93
+ const html = renderToStaticMarkup(
94
+ <ConsentCard
95
+ agentName="Cursor Agent"
96
+ agent="cursor-agent"
97
+ profile="full-local"
98
+ onApprove={noop}
99
+ onCancel={noop}
100
+ />,
101
+ );
102
+ expect(html).toContain("Run using your Cursor Agent setup?");
103
+ expect(html).toContain("Continue with my agent setup");
104
+ expect(html).toContain("border-amber-500/40");
105
+ expect(html).toContain("text-amber-500");
106
+ expect(html).toContain("Radar cannot constrain");
107
+ expect(html).toContain("always loads your global MCP servers");
108
+ expect(html).not.toContain("text-accent");
109
+ });
110
+
111
+ it("does not claim Radar enables a sandbox for Claude's normal setup", () => {
112
+ const html = renderToStaticMarkup(
113
+ <ConsentCard
114
+ agentName="Claude Code"
115
+ agent="claude"
116
+ profile="full-local"
117
+ onApprove={noop}
118
+ onCancel={noop}
119
+ />,
120
+ );
121
+ expect(html).toContain("permissions from your setup");
122
+ expect(html).toContain("Radar does not override them");
123
+ expect(html).not.toContain("Radar still enables the agent CLI");
124
+ });
125
+ });
@@ -27,13 +27,14 @@ import {
27
27
  type Diagnosis,
28
28
  type DiagnoseStep,
29
29
  type AgentInfo,
30
+ type ExecutionProfile,
30
31
  type RunSummary,
31
32
  } from "../../api/diagnose";
32
33
  import { StatusDot } from "@skyhook-io/k8s-ui";
33
34
  import { Markdown } from "../ui/Markdown";
34
35
 
35
- // Segmented two-or-more-way selector — shared shape for the agent picker and the
36
- // isolation toggle.
36
+ // Segmented two-or-more-way selector — shared shape for the agent and execution
37
+ // profile pickers.
37
38
  function Segmented<T extends string | boolean>({
38
39
  label,
39
40
  options,
@@ -228,15 +229,15 @@ function SelectMenu({
228
229
  );
229
230
  }
230
231
 
231
- // AgentControls is the full AI-diagnosis config block (agent, isolation, model,
232
+ // AgentControls is the full AI-diagnosis config block (agent, execution profile, model,
232
233
  // effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
233
234
  // since these are set-once preferences rather than per-run knobs.
234
235
  export function AgentControls({
235
236
  agents,
236
237
  selectedAgent,
237
238
  onSelectAgent,
238
- isolated,
239
- onSetIsolated,
239
+ profile,
240
+ onSetProfile,
240
241
  model,
241
242
  onSetModel,
242
243
  effort,
@@ -245,8 +246,8 @@ export function AgentControls({
245
246
  agents: AgentInfo[];
246
247
  selectedAgent: string;
247
248
  onSelectAgent: (name: string) => void;
248
- isolated: boolean;
249
- onSetIsolated: (v: boolean) => void;
249
+ profile: ExecutionProfile;
250
+ onSetProfile: (v: ExecutionProfile) => void;
250
251
  model: string;
251
252
  onSetModel: (v: string) => void;
252
253
  effort: string;
@@ -255,6 +256,17 @@ export function AgentControls({
255
256
  const isCodex = selectedAgent === "codex";
256
257
  const isClaude = selectedAgent === "claude";
257
258
  const isCursor = selectedAgent === "cursor-agent";
259
+ const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
260
+ const selectedAgentLabel =
261
+ selectedAgentInfo?.label || selectedAgent || "agent";
262
+ const profiles = selectedAgentInfo?.profiles ?? [];
263
+ const shownProfile = profiles.includes(profile)
264
+ ? profile
265
+ : (profiles[0] ?? profile);
266
+ const profileLabels: Record<ExecutionProfile, string> = {
267
+ safeguarded: "Radar safeguards",
268
+ "full-local": `Your ${selectedAgentLabel} setup`,
269
+ };
258
270
  return (
259
271
  <div className="space-y-3">
260
272
  {agents.length >= 2 && (
@@ -268,29 +280,66 @@ export function AgentControls({
268
280
  }))}
269
281
  />
270
282
  )}
271
- {isCodex && (
283
+ {profiles.length > 0 && (
272
284
  <div>
273
- <Segmented<boolean>
274
- label="Environment"
275
- value={isolated}
276
- onChange={onSetIsolated}
277
- options={[
278
- { value: true, label: "Isolated (recommended)" },
279
- { value: false, label: "My setup" },
280
- ]}
281
- />
282
- {isolated ? (
283
- <p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
284
- Runs Codex on its own — no access to your other MCP servers,
285
- guidelines, or project files.
286
- </p>
285
+ {profiles.length > 1 ? (
286
+ <>
287
+ <Segmented<ExecutionProfile>
288
+ label="How Radar runs it"
289
+ value={shownProfile}
290
+ onChange={onSetProfile}
291
+ options={profiles.map((value) => ({
292
+ value,
293
+ label: profileLabels[value],
294
+ }))}
295
+ />
296
+ {shownProfile === "safeguarded" ? (
297
+ <p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
298
+ {isClaude
299
+ ? "Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
300
+ : isCodex
301
+ ? "Radar excludes your Codex configuration and other MCP servers. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
302
+ : "Radar uses this agent’s safeguarded execution profile. Review the agent’s documented restrictions before continuing."}
303
+ </p>
304
+ ) : (
305
+ <div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
306
+ <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
307
+ <span>
308
+ Uses your agent&apos;s normal configuration and other
309
+ configured tools and MCP servers. Radar cannot constrain that
310
+ external tooling; it may access local files or the network
311
+ and may be able to change your cluster.{" "}
312
+ {isClaude
313
+ ? "Claude uses the permissions from your setup; Radar does not override them."
314
+ : "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
315
+ Choose this only when you need that setup.
316
+ </span>
317
+ </div>
318
+ )}
319
+ </>
320
+ ) : shownProfile === "safeguarded" ? (
321
+ <div className="flex items-start gap-1.5 rounded border border-theme-border bg-theme-base p-2 text-[11px] leading-snug text-theme-text-secondary">
322
+ <ShieldCheck className="mt-0.5 h-3 w-3 shrink-0 text-accent" />
323
+ <span>
324
+ Radar always runs this agent with safeguards.
325
+ {isClaude
326
+ ? " Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
327
+ : isCodex
328
+ ? " Your Codex configuration and other MCP servers are excluded. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
329
+ : " Review the agent’s documented restrictions before continuing."}
330
+ </span>
331
+ </div>
287
332
  ) : (
288
- <div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
333
+ <div className="flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
289
334
  <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
290
335
  <span>
291
- Runs Codex with your full setup your own MCP servers (which may
292
- be write- or network-capable) and guidelines, and it can read
293
- local files. Only choose this if you rely on that config.
336
+ Radar must use this agent&apos;s normal setup. Radar cannot
337
+ constrain its external tools or MCP servers; they may access
338
+ local files or the network and may be able to change your
339
+ cluster. Radar still enables the agent CLI&apos;s own sandbox,
340
+ but that sandbox does not constrain external MCP servers.
341
+ {isCursor &&
342
+ " Cursor always loads your global MCP servers, so Radar cannot exclude them."}
294
343
  </span>
295
344
  </div>
296
345
  )}
@@ -304,7 +353,7 @@ export function AgentControls({
304
353
  onChange={onSetModel}
305
354
  hint="Aliases always resolve to the latest of that tier."
306
355
  />
307
- ) : (
356
+ ) : isCodex || isCursor ? (
308
357
  <TextField
309
358
  label="Model"
310
359
  value={model}
@@ -317,11 +366,19 @@ export function AgentControls({
317
366
  hint={
318
367
  isCursor
319
368
  ? "Leave empty for your Cursor default, or enter a model slug Cursor supports."
320
- : !isolated
321
- ? "“My setup uses your own Codex config's model; set a slug here to override it."
369
+ : shownProfile === "full-local"
370
+ ? "Your Codex setup uses its configured model; set a slug here to override it."
322
371
  : "Leave empty for Codex's default, or enter a model your Codex version supports."
323
372
  }
324
373
  />
374
+ ) : (
375
+ <TextField
376
+ label="Model"
377
+ value={model}
378
+ placeholder="Default"
379
+ onChange={onSetModel}
380
+ hint="Leave empty for the agent's default, or enter a model identifier it supports."
381
+ />
325
382
  )}
326
383
  {isCodex && (
327
384
  <SelectMenu
@@ -591,10 +648,12 @@ function ConsentCardShell({
591
648
  bullets,
592
649
  settingsLabel,
593
650
  approveLabel = "Approve & investigate",
651
+ warning = false,
594
652
  onOpenSettings,
595
653
  onApprove,
596
654
  onCancel,
597
655
  }: DiagnoseConsentCopy & {
656
+ warning?: boolean;
598
657
  onOpenSettings?: () => void;
599
658
  onApprove: () => void;
600
659
  onCancel: () => void;
@@ -606,9 +665,19 @@ function ConsentCardShell({
606
665
  ? "Change the agent and how it runs in Settings"
607
666
  : settingsLabel;
608
667
  return (
609
- <div className="rounded-lg border border-theme-border bg-theme-elevated p-4">
668
+ <div
669
+ className={
670
+ warning
671
+ ? "rounded-lg border border-amber-500/40 bg-amber-500/10 p-4"
672
+ : "rounded-lg border border-theme-border bg-theme-elevated p-4"
673
+ }
674
+ >
610
675
  <div className="mb-2 flex items-center gap-2">
611
- <ShieldCheck className="h-4 w-4 text-accent" />
676
+ {warning ? (
677
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
678
+ ) : (
679
+ <ShieldCheck className="h-4 w-4 text-accent" />
680
+ )}
612
681
  <div className="text-sm font-medium text-theme-text-primary">
613
682
  {title}
614
683
  </div>
@@ -660,7 +729,7 @@ function ConsentCardShell({
660
729
  export function ConsentCard({
661
730
  agentName,
662
731
  agent,
663
- isolated = true,
732
+ profile,
664
733
  copy,
665
734
  onOpenSettings,
666
735
  onApprove,
@@ -668,7 +737,7 @@ export function ConsentCard({
668
737
  }: {
669
738
  agentName: string;
670
739
  agent?: string;
671
- isolated?: boolean;
740
+ profile: ExecutionProfile;
672
741
  copy?: DiagnoseConsentCopy;
673
742
  onOpenSettings?: () => void;
674
743
  onApprove: () => void;
@@ -679,17 +748,19 @@ export function ConsentCard({
679
748
  // Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
680
749
  if (copy) return <ConsentCardShell {...copy} {...chrome} />;
681
750
 
682
- // Tier 2: OSS BYO-local default. Cursor can't be isolated (no flag suppresses
683
- // its global MCP servers), so it gets its own honest framing rather than the
684
- // isolated/my-setup pair.
685
- const isCursor = agent === "cursor-agent";
686
751
  return (
687
752
  <ConsentCardShell
688
753
  {...chrome}
754
+ warning={profile === "full-local"}
755
+ approveLabel={
756
+ profile === "full-local"
757
+ ? "Continue with my agent setup"
758
+ : "Approve & investigate"
759
+ }
689
760
  title={
690
- isolated && !isCursor
691
- ? "Run a read-only AI investigation?"
692
- : "Run an AI investigation?"
761
+ profile === "safeguarded"
762
+ ? "Run an AI investigation with Radar safeguards?"
763
+ : `Run using your ${agentName} setup?`
693
764
  }
694
765
  body={
695
766
  <>
@@ -698,48 +769,68 @@ export function ConsentCard({
698
769
  your own {agentName}
699
770
  </span>{" "}
700
771
  on your machine — no Radar cloud, no API key, no account. Radar sends
701
- this resource&apos;s spec, recent events, and pod logs to it (and on to
702
- its model provider under your account, not to Radar). Transcripts are
703
- kept in your local Radar history on this machine until cleared.
704
- {isolated && !isCursor && (
772
+ this resource&apos;s spec, recent events, and pod logs to it (and on
773
+ to its model provider under your account, not to Radar). Transcripts
774
+ are kept in your local Radar history on this machine until cleared.
775
+ {profile === "safeguarded" && (
705
776
  <>
706
777
  {" "}
707
- Through Radar the agent can only{" "}
708
- <span className="font-medium">read</span> it cannot change your
709
- cluster.
778
+ Radar&apos;s investigation tools can only{" "}
779
+ <span className="font-medium">read</span> your cluster.
710
780
  </>
711
781
  )}
712
782
  </>
713
783
  }
714
- bullets={[
715
- isCursor ? (
716
- <>
717
- Through Radar the agent only{" "}
718
- <span className="font-medium">reads</span> your cluster. But Cursor
719
- also loads your own global MCP servers and Radar can&apos;t exclude
720
- them (unlike Claude or Codex), so if any of those can make changes,
721
- Cursor could use them.
722
- </>
723
- ) : isolated ? (
724
- <>
725
- Isolated: only Radar&apos;s read-only investigation tools — your
726
- other CLI config and MCP servers are excluded.
727
- {agent === "codex" && (
784
+ bullets={
785
+ profile === "safeguarded"
786
+ ? [
787
+ agent === "claude" ? (
788
+ <>
789
+ Radar safeguards disable Claude&apos;s built-in tools and limit
790
+ MCP access to Radar&apos;s read-only investigation tools. Your
791
+ Claude settings, hooks, and CLAUDE.md instructions still apply
792
+ and are outside Radar&apos;s control.
793
+ </>
794
+ ) : agent === "codex" ? (
795
+ <>
796
+ Radar safeguards exclude your Codex configuration and other MCP
797
+ servers. Codex&apos;s sandboxed shell can still read files on
798
+ this machine; it cannot write or reach the network.
799
+ </>
800
+ ) : (
801
+ <>
802
+ Radar uses this agent&apos;s safeguarded execution profile.
803
+ Review the agent&apos;s documented restrictions before
804
+ continuing.
805
+ </>
806
+ ),
807
+ ]
808
+ : [
728
809
  <>
729
- {" "}
730
- Codex&apos;s sandboxed shell can still <em>read</em> files on
731
- your machine (it cannot write or reach the network).
732
- </>
733
- )}
734
- </>
735
- ) : (
736
- <>
737
- &ldquo;My setup&rdquo;: the agent also runs with your own CLI config
738
- + MCP servers and can read local files. Only Radar&apos;s own tools
739
- are read-only.
740
- </>
741
- ),
742
- ]}
810
+ Radar cannot constrain the agent&apos;s other configured tools or
811
+ MCP servers. They may access local files or the network and may
812
+ be able to change your cluster.
813
+ </>,
814
+ agent === "claude" ? (
815
+ <>
816
+ Claude uses the permissions from your setup; Radar does not
817
+ override them.
818
+ </>
819
+ ) : (
820
+ <>
821
+ Radar still enables the agent CLI&apos;s own sandbox, but that
822
+ sandbox does not constrain external MCP servers.
823
+ {agent === "cursor-agent" && (
824
+ <>
825
+ {" "}
826
+ Cursor always loads your global MCP servers; Radar cannot
827
+ exclude them.
828
+ </>
829
+ )}
830
+ </>
831
+ ),
832
+ ]
833
+ }
743
834
  />
744
835
  );
745
836
  }