@skyhook-io/radar-app 1.14.1 → 1.14.3
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.
- package/package.json +2 -2
- package/src/api/client.ts +30 -0
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1560 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/parts.test.tsx +3 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { AlertTriangle, Check, ShieldCheck, ChevronDown } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
TRANSITION_MENU,
|
|
5
|
+
overlayExitMs,
|
|
6
|
+
overlayTransitionStyle,
|
|
7
|
+
} from "../../utils/animation";
|
|
8
|
+
import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
|
|
9
|
+
import type { DiagnoseConsentCopy } from "../../context/DiagnoseCustomization";
|
|
10
|
+
import type { AgentInfo, ExecutionProfile } from "../../api/diagnose";
|
|
11
|
+
|
|
12
|
+
const CURSOR_FULL_LOCAL_WARNING =
|
|
13
|
+
"Radar passes Cursor --force, which auto-approves its built-in tools and every MCP server it loads, including your global servers. Cursor’s sandbox does not reliably confine those tools to Radar’s temporary workspace.";
|
|
14
|
+
|
|
15
|
+
// Segmented two-or-more-way selector — shared shape for the agent and execution
|
|
16
|
+
// profile pickers.
|
|
17
|
+
export function Segmented<T extends string | boolean>({
|
|
18
|
+
label,
|
|
19
|
+
options,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
}: {
|
|
23
|
+
label?: string;
|
|
24
|
+
options: { value: T; label: string }[];
|
|
25
|
+
value: T;
|
|
26
|
+
onChange: (v: T) => void;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
{label && (
|
|
31
|
+
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
32
|
+
{label}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<div className="flex gap-1 rounded-lg border border-theme-border bg-theme-base p-1">
|
|
36
|
+
{options.map((o) => (
|
|
37
|
+
<button
|
|
38
|
+
key={String(o.value)}
|
|
39
|
+
onClick={() => onChange(o.value)}
|
|
40
|
+
className={`flex-1 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
|
|
41
|
+
o.value === value
|
|
42
|
+
? "selection-strong selection-text selection-ring"
|
|
43
|
+
: "text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{o.label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Option = { value: string; label: string; description?: string };
|
|
55
|
+
|
|
56
|
+
// Claude Code's --model takes version-stable ALIASES that always resolve to the
|
|
57
|
+
// user's installed latest of that tier (per `claude --help`), so this list never
|
|
58
|
+
// rots across model updates. "" = the agent's own default. Descriptions mirror
|
|
59
|
+
// Claude Code's own /model picker so the tradeoff is legible.
|
|
60
|
+
const CLAUDE_MODEL_OPTIONS: Option[] = [
|
|
61
|
+
{
|
|
62
|
+
value: "",
|
|
63
|
+
label: "Default",
|
|
64
|
+
description: "Use Claude Code's configured model",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
value: "opus",
|
|
68
|
+
label: "Opus",
|
|
69
|
+
description: "Most capable — best for complex problems",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
value: "sonnet",
|
|
73
|
+
label: "Sonnet",
|
|
74
|
+
description: "Balanced — efficient for routine work",
|
|
75
|
+
},
|
|
76
|
+
{ value: "haiku", label: "Haiku", description: "Fastest — quick checks" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Codex has no stable alias set and no way to enumerate models, and slugs change
|
|
80
|
+
// across versions — so we take a free-text override rather than a list that rots.
|
|
81
|
+
const EFFORT_OPTIONS: Option[] = [
|
|
82
|
+
{
|
|
83
|
+
value: "",
|
|
84
|
+
label: "Default",
|
|
85
|
+
description: "Recommended — Radar's default (medium)",
|
|
86
|
+
},
|
|
87
|
+
{ value: "low", label: "Low", description: "Fastest, least reasoning" },
|
|
88
|
+
{ value: "medium", label: "Medium", description: "Balanced depth" },
|
|
89
|
+
{ value: "high", label: "High", description: "Most thorough, slowest" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function TextField({
|
|
93
|
+
label,
|
|
94
|
+
value,
|
|
95
|
+
placeholder,
|
|
96
|
+
onChange,
|
|
97
|
+
hint,
|
|
98
|
+
}: {
|
|
99
|
+
label: string;
|
|
100
|
+
value: string;
|
|
101
|
+
placeholder?: string;
|
|
102
|
+
onChange: (v: string) => void;
|
|
103
|
+
hint?: string;
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<div>
|
|
107
|
+
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
108
|
+
{label}
|
|
109
|
+
</div>
|
|
110
|
+
<input
|
|
111
|
+
type="text"
|
|
112
|
+
value={value}
|
|
113
|
+
placeholder={placeholder}
|
|
114
|
+
onChange={(e) => onChange(e.target.value)}
|
|
115
|
+
className="w-full rounded-md border border-theme-border bg-theme-base px-2 py-1.5 text-xs text-theme-text-primary placeholder:text-theme-text-tertiary"
|
|
116
|
+
/>
|
|
117
|
+
{hint && (
|
|
118
|
+
<p className="mt-1 text-[11px] leading-snug text-theme-text-tertiary">
|
|
119
|
+
{hint}
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// SelectMenu is a themed dropdown (button + popover list) matching the app's other
|
|
127
|
+
// custom dropdowns — unlike a native <select> it renders option descriptions and
|
|
128
|
+
// stays on-theme in both light/dark.
|
|
129
|
+
function SelectMenu({
|
|
130
|
+
label,
|
|
131
|
+
value,
|
|
132
|
+
options,
|
|
133
|
+
onChange,
|
|
134
|
+
hint,
|
|
135
|
+
}: {
|
|
136
|
+
label: string;
|
|
137
|
+
value: string;
|
|
138
|
+
options: Option[];
|
|
139
|
+
onChange: (v: string) => void;
|
|
140
|
+
hint?: string;
|
|
141
|
+
}) {
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
143
|
+
// Presence outlives `open` by the menu exit so the list can fade out; the
|
|
144
|
+
// click-away backdrop only exists while logically open.
|
|
145
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(
|
|
146
|
+
open,
|
|
147
|
+
overlayExitMs("menu"),
|
|
148
|
+
);
|
|
149
|
+
const current = options.find((o) => o.value === value) ?? options[0];
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
153
|
+
{label}
|
|
154
|
+
</div>
|
|
155
|
+
<div className="relative">
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => setOpen((v) => !v)}
|
|
158
|
+
aria-haspopup="listbox"
|
|
159
|
+
aria-expanded={open}
|
|
160
|
+
className="flex w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-base px-2.5 py-1.5 text-left text-xs text-theme-text-primary hover:bg-theme-hover"
|
|
161
|
+
>
|
|
162
|
+
<span className="truncate">{current?.label}</span>
|
|
163
|
+
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
164
|
+
</button>
|
|
165
|
+
{open && (
|
|
166
|
+
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
|
167
|
+
)}
|
|
168
|
+
{shouldRender && (
|
|
169
|
+
<ul
|
|
170
|
+
role="listbox"
|
|
171
|
+
inert={!open || undefined}
|
|
172
|
+
className={`absolute left-0 right-0 z-20 mt-1 max-h-72 origin-top overflow-y-auto rounded-md border border-theme-border bg-theme-surface py-1 shadow-theme-lg ${TRANSITION_MENU} ${
|
|
173
|
+
isOpen
|
|
174
|
+
? "opacity-100 translate-y-0 scale-100"
|
|
175
|
+
: "opacity-0 -translate-y-1 scale-[0.97]"
|
|
176
|
+
} ${open ? "" : "pointer-events-none"}`}
|
|
177
|
+
style={overlayTransitionStyle(isOpen, "menu")}
|
|
178
|
+
>
|
|
179
|
+
{options.map((o) => {
|
|
180
|
+
const sel = o.value === value;
|
|
181
|
+
return (
|
|
182
|
+
<li key={o.value}>
|
|
183
|
+
<button
|
|
184
|
+
role="option"
|
|
185
|
+
aria-selected={sel}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
onChange(o.value);
|
|
188
|
+
setOpen(false);
|
|
189
|
+
}}
|
|
190
|
+
className="flex w-full items-start gap-2 px-2.5 py-1.5 text-left hover:bg-theme-hover"
|
|
191
|
+
>
|
|
192
|
+
<Check
|
|
193
|
+
className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${sel ? "text-accent" : "opacity-0"}`}
|
|
194
|
+
/>
|
|
195
|
+
<span className="min-w-0">
|
|
196
|
+
<span className="block text-xs font-medium text-theme-text-primary">
|
|
197
|
+
{o.label}
|
|
198
|
+
</span>
|
|
199
|
+
{o.description && (
|
|
200
|
+
<span className="block text-[11px] leading-snug text-theme-text-tertiary">
|
|
201
|
+
{o.description}
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</span>
|
|
205
|
+
</button>
|
|
206
|
+
</li>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</ul>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
{hint && (
|
|
213
|
+
<p className="mt-1 text-[11px] leading-snug text-theme-text-tertiary">
|
|
214
|
+
{hint}
|
|
215
|
+
</p>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// AgentControls is the full AI investigation config block (agent, execution profile, model,
|
|
222
|
+
// effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
|
|
223
|
+
// since these are set-once preferences rather than per-run knobs.
|
|
224
|
+
export function AgentControls({
|
|
225
|
+
agents,
|
|
226
|
+
selectedAgent,
|
|
227
|
+
onSelectAgent,
|
|
228
|
+
profile,
|
|
229
|
+
onSetProfile,
|
|
230
|
+
model,
|
|
231
|
+
onSetModel,
|
|
232
|
+
effort,
|
|
233
|
+
onSetEffort,
|
|
234
|
+
}: {
|
|
235
|
+
agents: AgentInfo[];
|
|
236
|
+
selectedAgent: string;
|
|
237
|
+
onSelectAgent: (name: string) => void;
|
|
238
|
+
profile: ExecutionProfile;
|
|
239
|
+
onSetProfile: (v: ExecutionProfile) => void;
|
|
240
|
+
model: string;
|
|
241
|
+
onSetModel: (v: string) => void;
|
|
242
|
+
effort: string;
|
|
243
|
+
onSetEffort: (v: string) => void;
|
|
244
|
+
}) {
|
|
245
|
+
const isCodex = selectedAgent === "codex";
|
|
246
|
+
const isClaude = selectedAgent === "claude";
|
|
247
|
+
const isCursor = selectedAgent === "cursor-agent";
|
|
248
|
+
const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
|
|
249
|
+
const selectedAgentLabel =
|
|
250
|
+
selectedAgentInfo?.label || selectedAgent || "agent";
|
|
251
|
+
const profiles = selectedAgentInfo?.profiles ?? [];
|
|
252
|
+
const shownProfile = profiles.includes(profile)
|
|
253
|
+
? profile
|
|
254
|
+
: (profiles[0] ?? profile);
|
|
255
|
+
const profileLabels: Record<ExecutionProfile, string> = {
|
|
256
|
+
safeguarded: "Radar safeguards",
|
|
257
|
+
"full-local": `Your ${selectedAgentLabel} setup`,
|
|
258
|
+
};
|
|
259
|
+
return (
|
|
260
|
+
<div className="space-y-3">
|
|
261
|
+
{agents.length >= 2 && (
|
|
262
|
+
<Segmented
|
|
263
|
+
label="Agent"
|
|
264
|
+
value={selectedAgent}
|
|
265
|
+
onChange={onSelectAgent}
|
|
266
|
+
options={agents.map((a) => ({
|
|
267
|
+
value: a.name,
|
|
268
|
+
label: a.label || a.name,
|
|
269
|
+
}))}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
{profiles.length > 0 && (
|
|
273
|
+
<div>
|
|
274
|
+
{profiles.length > 1 ? (
|
|
275
|
+
<>
|
|
276
|
+
<Segmented<ExecutionProfile>
|
|
277
|
+
label="How Radar runs it"
|
|
278
|
+
value={shownProfile}
|
|
279
|
+
onChange={onSetProfile}
|
|
280
|
+
options={profiles.map((value) => ({
|
|
281
|
+
value,
|
|
282
|
+
label: profileLabels[value],
|
|
283
|
+
}))}
|
|
284
|
+
/>
|
|
285
|
+
{shownProfile === "safeguarded" ? (
|
|
286
|
+
<p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
287
|
+
{isClaude
|
|
288
|
+
? "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."
|
|
289
|
+
: isCodex
|
|
290
|
+
? "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."
|
|
291
|
+
: "Radar uses this agent’s safeguarded execution profile. Review the agent’s documented restrictions before continuing."}
|
|
292
|
+
</p>
|
|
293
|
+
) : (
|
|
294
|
+
<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">
|
|
295
|
+
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
|
|
296
|
+
<span>
|
|
297
|
+
Uses your agent's normal configuration and other
|
|
298
|
+
configured tools and MCP servers. Radar cannot constrain
|
|
299
|
+
that external tooling; it may access local files or the
|
|
300
|
+
network and may be able to change your cluster.{" "}
|
|
301
|
+
{isCursor
|
|
302
|
+
? CURSOR_FULL_LOCAL_WARNING
|
|
303
|
+
: isClaude
|
|
304
|
+
? "Claude uses the permissions from your setup; Radar does not override them."
|
|
305
|
+
: "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
|
|
306
|
+
Choose this only when you need that setup.
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</>
|
|
311
|
+
) : shownProfile === "safeguarded" ? (
|
|
312
|
+
<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">
|
|
313
|
+
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0 text-accent" />
|
|
314
|
+
<span>
|
|
315
|
+
Radar always runs this agent with safeguards.
|
|
316
|
+
{isClaude
|
|
317
|
+
? " 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."
|
|
318
|
+
: isCodex
|
|
319
|
+
? " 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."
|
|
320
|
+
: " Review the agent’s documented restrictions before continuing."}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
) : (
|
|
324
|
+
<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">
|
|
325
|
+
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
|
|
326
|
+
<span>
|
|
327
|
+
Radar must use this agent's normal setup. Radar cannot
|
|
328
|
+
constrain its external tools or MCP servers; they may access
|
|
329
|
+
local files or the network and may be able to change your
|
|
330
|
+
cluster.{" "}
|
|
331
|
+
{isCursor ? (
|
|
332
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
333
|
+
) : (
|
|
334
|
+
<>
|
|
335
|
+
Radar still enables the agent CLI's own sandbox, but
|
|
336
|
+
that sandbox does not constrain external MCP servers.
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
</span>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
{isClaude ? (
|
|
345
|
+
<SelectMenu
|
|
346
|
+
label="Model"
|
|
347
|
+
value={model}
|
|
348
|
+
options={CLAUDE_MODEL_OPTIONS}
|
|
349
|
+
onChange={onSetModel}
|
|
350
|
+
hint="Aliases always resolve to the latest of that tier."
|
|
351
|
+
/>
|
|
352
|
+
) : isCodex || isCursor ? (
|
|
353
|
+
<TextField
|
|
354
|
+
label="Model"
|
|
355
|
+
value={model}
|
|
356
|
+
placeholder={
|
|
357
|
+
isCursor
|
|
358
|
+
? "Default (e.g. auto, gpt-5.2, composer-2.5)"
|
|
359
|
+
: "Default (e.g. gpt-5-codex, o3)"
|
|
360
|
+
}
|
|
361
|
+
onChange={onSetModel}
|
|
362
|
+
hint={
|
|
363
|
+
isCursor
|
|
364
|
+
? "Leave empty for your Cursor default, or enter a model slug Cursor supports."
|
|
365
|
+
: shownProfile === "full-local"
|
|
366
|
+
? "Your Codex setup uses its configured model; set a slug here to override it."
|
|
367
|
+
: "Leave empty for Codex's default, or enter a model your Codex version supports."
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
370
|
+
) : (
|
|
371
|
+
<TextField
|
|
372
|
+
label="Model"
|
|
373
|
+
value={model}
|
|
374
|
+
placeholder="Default"
|
|
375
|
+
onChange={onSetModel}
|
|
376
|
+
hint="Leave empty for the agent's default, or enter a model identifier it supports."
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
{isCodex && (
|
|
380
|
+
<SelectMenu
|
|
381
|
+
label="Reasoning effort"
|
|
382
|
+
value={effort}
|
|
383
|
+
options={EFFORT_OPTIONS}
|
|
384
|
+
onChange={onSetEffort}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ConsentCardShell owns the card chrome (icon, layout, settings link, Approve/
|
|
392
|
+
// Cancel) so every copy tier — the OSS default, the hosted fallback, and a host
|
|
393
|
+
// override — feeds the same frame instead of duplicating it.
|
|
394
|
+
function ConsentCardShell({
|
|
395
|
+
title,
|
|
396
|
+
body,
|
|
397
|
+
bullets,
|
|
398
|
+
settingsLabel,
|
|
399
|
+
approveLabel = "Approve & investigate",
|
|
400
|
+
warning = false,
|
|
401
|
+
error,
|
|
402
|
+
onOpenSettings,
|
|
403
|
+
onApprove,
|
|
404
|
+
onCancel,
|
|
405
|
+
}: DiagnoseConsentCopy & {
|
|
406
|
+
warning?: boolean;
|
|
407
|
+
// Why the last approval failed, straight from the server. The card stays up on
|
|
408
|
+
// failure so retrying works in place, which means this is the ONLY chance to
|
|
409
|
+
// say why — a host that records consent above the individual refuses the
|
|
410
|
+
// wrong person here, and only its message knows who to ask.
|
|
411
|
+
error?: string | null;
|
|
412
|
+
onOpenSettings?: () => void;
|
|
413
|
+
onApprove: () => void;
|
|
414
|
+
onCancel: () => void;
|
|
415
|
+
}) {
|
|
416
|
+
// `settingsLabel: null` hides the link outright — a host with one fixed agent
|
|
417
|
+
// has nothing behind it. `undefined` keeps the OSS default label.
|
|
418
|
+
const resolvedSettingsLabel =
|
|
419
|
+
settingsLabel === undefined
|
|
420
|
+
? "Change the agent and how it runs in Settings"
|
|
421
|
+
: settingsLabel;
|
|
422
|
+
return (
|
|
423
|
+
<div
|
|
424
|
+
className={
|
|
425
|
+
warning
|
|
426
|
+
? "rounded-lg border border-amber-500/40 bg-amber-500/10 p-4"
|
|
427
|
+
: "rounded-lg border border-theme-border bg-theme-elevated p-4"
|
|
428
|
+
}
|
|
429
|
+
>
|
|
430
|
+
<div className="mb-2 flex items-center gap-2">
|
|
431
|
+
{warning ? (
|
|
432
|
+
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
433
|
+
) : (
|
|
434
|
+
<ShieldCheck className="h-4 w-4 text-accent" />
|
|
435
|
+
)}
|
|
436
|
+
<div className="text-sm font-medium text-theme-text-primary">
|
|
437
|
+
{title}
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div className="text-sm leading-relaxed text-theme-text-secondary">
|
|
441
|
+
{body}
|
|
442
|
+
</div>
|
|
443
|
+
{bullets && bullets.length > 0 && (
|
|
444
|
+
<ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
|
|
445
|
+
{bullets.map((b, i) => (
|
|
446
|
+
<li key={i}>• {b}</li>
|
|
447
|
+
))}
|
|
448
|
+
</ul>
|
|
449
|
+
)}
|
|
450
|
+
{onOpenSettings && resolvedSettingsLabel && (
|
|
451
|
+
<button
|
|
452
|
+
onClick={onOpenSettings}
|
|
453
|
+
className="mt-3 text-xs text-accent hover:underline"
|
|
454
|
+
>
|
|
455
|
+
{resolvedSettingsLabel}
|
|
456
|
+
</button>
|
|
457
|
+
)}
|
|
458
|
+
{/* Red, not amber: the full-local card is itself amber, so an amber box on
|
|
459
|
+
it reads as another paragraph of body copy. role="alert" because nothing
|
|
460
|
+
else moves on failure — focus stays on Approve, so without it a screen
|
|
461
|
+
reader says nothing and the user re-presses a button that cannot succeed. */}
|
|
462
|
+
{error && (
|
|
463
|
+
<div
|
|
464
|
+
role="alert"
|
|
465
|
+
className="mt-3 flex items-start gap-2 rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-theme-text-primary"
|
|
466
|
+
>
|
|
467
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-400" />
|
|
468
|
+
<span>{error}</span>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
<div className="mt-4 flex gap-2">
|
|
472
|
+
<button
|
|
473
|
+
onClick={onCancel}
|
|
474
|
+
className="flex-1 rounded-lg border border-theme-border py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
|
|
475
|
+
>
|
|
476
|
+
Cancel
|
|
477
|
+
</button>
|
|
478
|
+
<button
|
|
479
|
+
onClick={onApprove}
|
|
480
|
+
className="flex-1 rounded-lg btn-brand py-1.5 text-sm"
|
|
481
|
+
>
|
|
482
|
+
{approveLabel}
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// The first-run consent + trust card. The copy is checkable fact about a data
|
|
490
|
+
// flow — not marketing — and the wrong claim is a lie, not a typo. It resolves
|
|
491
|
+
// in two tiers:
|
|
492
|
+
//
|
|
493
|
+
// 1. `copy` — the embedding host tells its own trust story. Only the host
|
|
494
|
+
// knows where its agent runs, whose key pays, and where transcripts live,
|
|
495
|
+
// so any host that runs the agent somewhere other than the OSS local CLI
|
|
496
|
+
// MUST override rather than let Radar assert the local story over its flow.
|
|
497
|
+
// 2. No `copy` — the OSS bring-your-own-local-CLI default, the only tier where
|
|
498
|
+
// "on your machine / no Radar cloud / your account" actually holds.
|
|
499
|
+
export function ConsentCard({
|
|
500
|
+
agentName,
|
|
501
|
+
agent,
|
|
502
|
+
profile,
|
|
503
|
+
copy,
|
|
504
|
+
error,
|
|
505
|
+
onOpenSettings,
|
|
506
|
+
onApprove,
|
|
507
|
+
onCancel,
|
|
508
|
+
}: {
|
|
509
|
+
agentName: string;
|
|
510
|
+
agent?: string;
|
|
511
|
+
profile: ExecutionProfile;
|
|
512
|
+
copy?: DiagnoseConsentCopy;
|
|
513
|
+
error?: string | null;
|
|
514
|
+
onOpenSettings?: () => void;
|
|
515
|
+
onApprove: () => void;
|
|
516
|
+
onCancel: () => void;
|
|
517
|
+
}) {
|
|
518
|
+
const chrome = { onOpenSettings, onApprove, onCancel, error };
|
|
519
|
+
|
|
520
|
+
// Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
|
|
521
|
+
if (copy) return <ConsentCardShell {...copy} {...chrome} />;
|
|
522
|
+
|
|
523
|
+
return (
|
|
524
|
+
<ConsentCardShell
|
|
525
|
+
{...chrome}
|
|
526
|
+
warning={profile === "full-local"}
|
|
527
|
+
approveLabel={
|
|
528
|
+
profile === "full-local"
|
|
529
|
+
? "Continue with my agent setup"
|
|
530
|
+
: "Approve & investigate"
|
|
531
|
+
}
|
|
532
|
+
title={
|
|
533
|
+
profile === "safeguarded"
|
|
534
|
+
? "Run an AI investigation with Radar safeguards?"
|
|
535
|
+
: `Run using your ${agentName} setup?`
|
|
536
|
+
}
|
|
537
|
+
body={
|
|
538
|
+
<>
|
|
539
|
+
This runs{" "}
|
|
540
|
+
<span className="font-medium text-theme-text-primary">
|
|
541
|
+
your own {agentName}
|
|
542
|
+
</span>{" "}
|
|
543
|
+
on your machine — no Radar cloud, no API key, no account. Radar sends
|
|
544
|
+
this resource's spec, recent events, and pod logs to it (and on
|
|
545
|
+
to its model provider under your account, not to Radar). Transcripts
|
|
546
|
+
are kept in your local Radar history on this machine until cleared.
|
|
547
|
+
{profile === "safeguarded" && (
|
|
548
|
+
<>
|
|
549
|
+
{" "}
|
|
550
|
+
Radar's investigation tools can only{" "}
|
|
551
|
+
<span className="font-medium">read</span> your cluster.
|
|
552
|
+
</>
|
|
553
|
+
)}
|
|
554
|
+
</>
|
|
555
|
+
}
|
|
556
|
+
bullets={
|
|
557
|
+
profile === "safeguarded"
|
|
558
|
+
? [
|
|
559
|
+
agent === "claude" ? (
|
|
560
|
+
<>
|
|
561
|
+
Radar safeguards disable Claude's built-in tools and
|
|
562
|
+
limit MCP access to Radar's read-only investigation
|
|
563
|
+
tools. Your Claude settings, hooks, and CLAUDE.md instructions
|
|
564
|
+
still apply and are outside Radar's control.
|
|
565
|
+
</>
|
|
566
|
+
) : agent === "codex" ? (
|
|
567
|
+
<>
|
|
568
|
+
Radar safeguards exclude your Codex configuration and other
|
|
569
|
+
MCP servers. Codex's sandboxed shell can still read files
|
|
570
|
+
on this machine; it cannot write or reach the network.
|
|
571
|
+
</>
|
|
572
|
+
) : (
|
|
573
|
+
<>
|
|
574
|
+
Radar uses this agent's safeguarded execution profile.
|
|
575
|
+
Review the agent's documented restrictions before
|
|
576
|
+
continuing.
|
|
577
|
+
</>
|
|
578
|
+
),
|
|
579
|
+
]
|
|
580
|
+
: [
|
|
581
|
+
<>
|
|
582
|
+
Radar cannot constrain the agent's other configured tools
|
|
583
|
+
or MCP servers. They may access local files or the network and
|
|
584
|
+
may be able to change your cluster.
|
|
585
|
+
</>,
|
|
586
|
+
agent === "cursor-agent" ? (
|
|
587
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
588
|
+
) : agent === "claude" ? (
|
|
589
|
+
<>
|
|
590
|
+
Claude uses the permissions from your setup; Radar does not
|
|
591
|
+
override them.
|
|
592
|
+
</>
|
|
593
|
+
) : (
|
|
594
|
+
<>
|
|
595
|
+
Radar still enables the agent CLI's own sandbox, but that
|
|
596
|
+
sandbox does not constrain external MCP servers.
|
|
597
|
+
</>
|
|
598
|
+
),
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
/>
|
|
602
|
+
);
|
|
603
|
+
}
|