@skyhook-io/radar-app 1.14.2 → 1.14.4

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 (83) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +32 -0
  3. package/src/api/diagnose.ts +7 -1
  4. package/src/components/CloudConnectFlow.tsx +21 -7
  5. package/src/components/CloudFunnelButton.tsx +85 -8
  6. package/src/components/cloudConnectHandoff.test.ts +10 -1
  7. package/src/components/cloudConnectHandoff.ts +15 -1
  8. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  9. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  10. package/src/components/diagnose/AgentControls.tsx +603 -0
  11. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  12. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  13. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  14. package/src/components/diagnose/AssessmentCard.tsx +1579 -0
  15. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  16. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  17. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  18. package/src/components/diagnose/Home.test.tsx +1 -3
  19. package/src/components/diagnose/Home.tsx +2 -1
  20. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  21. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  22. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  23. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  24. package/src/components/diagnose/InvestigationView.tsx +39 -51
  25. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  26. package/src/components/diagnose/assessmentCopy.ts +169 -0
  27. package/src/components/diagnose/investigationCase.test.tsx +658 -2
  28. package/src/components/diagnose/investigationCase.ts +135 -26
  29. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +682 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
  31. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
  32. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  34. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +136 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  41. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  47. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  48. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  49. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  50. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  51. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  52. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  53. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  54. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  55. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  57. package/src/components/diagnose/investigationEvidence/observations.ts +13 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  59. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  60. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  61. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  62. package/src/components/diagnose/investigationState.test.ts +2 -2
  63. package/src/components/diagnose/investigationState.ts +2 -2
  64. package/src/components/diagnose/investigationStory.ts +1 -1
  65. package/src/components/diagnose/parts.test.tsx +15 -1
  66. package/src/components/diagnose/parts.tsx +26 -3904
  67. package/src/components/diagnose/target.ts +9 -3
  68. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  69. package/src/components/diagnose/toolCallLabel.ts +41 -2
  70. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  71. package/src/components/home/MCPSetupDialog.tsx +1 -1
  72. package/src/components/home/mcpToolCatalog.ts +27 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  74. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  75. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  76. package/src/components/resources/PodFilePreview.tsx +1 -1
  77. package/src/components/rightsizing/RightsizingScanView.tsx +20 -1
  78. package/src/components/ui/CommandPalette.tsx +12 -8
  79. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  80. package/src/components/ui/Omnibar.tsx +96 -60
  81. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  82. package/src/components/workload/WorkloadView.tsx +7 -1
  83. package/src/components/ui/Disclosure.tsx +0 -47
@@ -1,5 +1,3 @@
1
- import { pluralToKind } from "@skyhook-io/k8s-ui";
2
-
3
1
  import type {
4
2
  DiagnosisEvidenceItem,
5
3
  DiagnosisEvidenceRole,
@@ -10,6 +8,8 @@ import {
10
8
  investigationEvidenceSubjectRef,
11
9
  investigationSourceArgs,
12
10
  isInvestigationEvidenceRef,
11
+ sameKind,
12
+ type InvestigationEvidenceData,
13
13
  type InvestigationEvidenceGroup,
14
14
  type InvestigationEvidenceObservation,
15
15
  type InvestigationEvidenceProjection,
@@ -54,7 +54,7 @@ export interface InvestigationCaseResolution {
54
54
  }
55
55
 
56
56
  /**
57
- * One half of a Go↔TS contract: `evidenceRoles` in internal/ai/parse.go and the
57
+ * One half of a Go↔TS contract: `Roles` in pkg/investigation/verdict.go and the
58
58
  * DiagnosisEvidenceRole union in api/diagnose.ts must list exactly these roles.
59
59
  * A role the parser accepts but this set omits is bound server-side and then
60
60
  * silently dropped here. Change all three together.
@@ -140,22 +140,23 @@ export function resolveInvestigationCase(
140
140
  source,
141
141
  placement: "source",
142
142
  };
143
+ const produced = projection.groups.flatMap((group) =>
144
+ group.observations
145
+ .filter((observation) => observation.source.id === source.id)
146
+ .map((observation) => ({ group, observation })),
147
+ );
143
148
  const candidates = subjectUnusable
144
149
  ? []
145
- : projection.groups.flatMap((group) =>
146
- group.observations
147
- .filter(
148
- (observation) =>
149
- observation.source.id === source.id &&
150
- (!subject ||
151
- observationMatchesSubject(
152
- group,
153
- observation,
154
- subject,
155
- targetPods,
156
- )),
157
- )
158
- .map((observation) => ({ group, observation })),
150
+ : produced.filter(
151
+ ({ group, observation }) =>
152
+ !subject ||
153
+ observationMatchesSubject(
154
+ group,
155
+ observation,
156
+ subject,
157
+ targetPods,
158
+ produced.length === 1,
159
+ ),
159
160
  );
160
161
  if (candidates.length === 1) {
161
162
  const { group, observation } = candidates[0];
@@ -276,6 +277,19 @@ function observationSubjectIdentity(
276
277
  const args = investigationSourceArgs(observation.source);
277
278
  // A listing whose call names no kind (list_namespaces) is still a listing
278
279
  // of the one kind its rows carry.
280
+ // A ranking is a listing of what it ranked; a posture card is a listing of
281
+ // the resources its findings are about.
282
+ if (data.type === "ranking" || data.type === "posture") {
283
+ const rows = data.type === "ranking" ? data.rows : data.findings;
284
+ const kinds = new Set(rows.map((row) => row.kind));
285
+ if (kinds.size !== 1) return undefined;
286
+ return {
287
+ kind: [...kinds][0],
288
+ namespace: nonEmptyString(args?.namespace) ? args.namespace : undefined,
289
+ name: "",
290
+ listing: true,
291
+ };
292
+ }
279
293
  if (data.type === "inventory" && !nonEmptyString(args?.kind)) {
280
294
  const kinds = new Set(data.resources.map((resource) => resource.kind));
281
295
  if (kinds.size !== 1) return undefined;
@@ -311,31 +325,76 @@ function isBuiltInGroup(group: string): boolean {
311
325
  return LEGACY_BUILT_IN_GROUPS.has(group) || group.endsWith(".k8s.io");
312
326
  }
313
327
 
314
- export function sameKind(left: string, right: string): boolean {
315
- return pluralToKind(left).toLowerCase() === pluralToKind(right).toLowerCase();
328
+ export { sameKind };
329
+
330
+ /**
331
+ * Whether a listing row is the entry a citation names. A package row carries
332
+ * a kind of Radar's own, so the agent names it by the kind it knows the
333
+ * package as (the Flux HelmRelease, the chart); every other row is named by
334
+ * its kind.
335
+ */
336
+ export function listingRowNamesSubject(
337
+ row: { kind: string; name: string },
338
+ subject: { kind: string; name?: string },
339
+ ): boolean {
340
+ return (
341
+ subject.name !== undefined &&
342
+ row.name === subject.name &&
343
+ (row.kind === "Package" || sameKind(row.kind, subject.kind))
344
+ );
316
345
  }
317
346
 
347
+ // The vocabulary the agent places with names evidence kinds a reader would
348
+ // distinguish (logs, events, changes, metrics, alerts, issue); everything a
349
+ // call returns about a resource is "resource" to it, whether Radar draws that
350
+ // as a resource card, a listing, a ranking, a posture card, a Helm release, a
351
+ // permissions check or a neighborhood. The kinds a diagnose bundle yields
352
+ // beside its resource (startup, crash, dns, network, receipts) stay out so
353
+ // "resource" still picks one observation of that bundle.
354
+ const RESOURCE_SHAPED: ReadonlySet<InvestigationEvidenceData["type"]> = new Set<
355
+ InvestigationEvidenceData["type"]
356
+ >([
357
+ "resource",
358
+ "inventory",
359
+ "ranking",
360
+ "posture",
361
+ "helm",
362
+ "permissions",
363
+ "topology",
364
+ "relationships",
365
+ ]);
366
+
318
367
  /**
319
368
  * A discriminator the agent omits is a wildcard, and so is one the producer
320
369
  * did not state; every discriminator both sides supply must match. Uniqueness
321
370
  * of the match, not completeness of the subject, is what places a claim.
371
+ *
372
+ * The evidence word tells observations of one call apart, and a receipt is
373
+ * not a kind of its own: it is what a call of any kind returned when it found
374
+ * nothing (a metric search, a scan with nothing about the target). A call
375
+ * whose only observation is a receipt is not held to the word; what the
376
+ * subject names still has to match.
322
377
  */
323
378
  function observationMatchesSubject(
324
379
  group: InvestigationEvidenceGroup,
325
380
  observation: InvestigationEvidenceObservation,
326
381
  subject: DiagnosisEvidenceSubject,
327
382
  targetPods: ReadonlySet<string>,
383
+ sole: boolean,
328
384
  ): boolean {
329
- if (subject.observation !== undefined) {
385
+ const soleReceipt = sole && observation.data.type === "receipt";
386
+ if (subject.observation !== undefined && !soleReceipt) {
330
387
  // A diagnose bundle captures several vitals charts for one resource, so
331
388
  // "metrics" alone cannot name one; "metrics:<category>" picks the chart
332
389
  // whose identity ends in that category. An agent-run query is one chart,
333
390
  // so a qualifier on it carries no meaning.
334
- const [kind, qualifier] = subject.observation.toLowerCase().split(":", 2);
335
- // A listing is resources too: "resource" names an inventory card as well.
336
- const inventoryAsResource =
337
- kind === "resource" && observation.data.type === "inventory";
338
- if (kind !== observation.data.type && !inventoryAsResource) return false;
391
+ const [word, qualifier] = subject.observation.toLowerCase().split(":", 2);
392
+ const kind = word === "issues" ? "issue" : word;
393
+ if (
394
+ kind !== observation.data.type &&
395
+ !(kind === "resource" && RESOURCE_SHAPED.has(observation.data.type))
396
+ )
397
+ return false;
339
398
  if (
340
399
  qualifier !== undefined &&
341
400
  observation.data.type === "metrics" &&
@@ -371,6 +430,36 @@ function observationIdentities(
371
430
  ): ObservationSubjectIdentity[] {
372
431
  const stated = observationSubjectIdentity(observation);
373
432
  const identities = stated ? [stated] : [];
433
+ // A ranking is also each row it ranks and the workload each pod belongs
434
+ // to; a posture card each resource its findings name and the workload that
435
+ // owns it: a citation of the workload reaches its pods' rows and the
436
+ // finding on its HPA, and a card of mixed kinds accepts no name it does
437
+ // not hold.
438
+ if (
439
+ observation.data.type === "ranking" ||
440
+ observation.data.type === "posture"
441
+ ) {
442
+ const rows =
443
+ observation.data.type === "ranking"
444
+ ? observation.data.rows.flatMap((row) =>
445
+ row.owner ? [row, row.owner] : [row],
446
+ )
447
+ : observation.data.findings.flatMap((finding) =>
448
+ finding.managedBy ? [finding, finding.managedBy] : [finding],
449
+ );
450
+ const seen = new Set<string>();
451
+ for (const row of rows) {
452
+ const key = `${row.kind}/${row.namespace ?? ""}/${row.name}`;
453
+ if (seen.has(key)) continue;
454
+ seen.add(key);
455
+ identities.push({
456
+ kind: row.kind,
457
+ group: "group" in row ? row.group : undefined,
458
+ namespace: row.namespace ?? "",
459
+ name: row.name,
460
+ });
461
+ }
462
+ }
374
463
  if (observation.data.type === "logs" && stated) {
375
464
  const args = investigationSourceArgs(observation.source);
376
465
  if (args && nonEmptyString(args.kind) && nonEmptyString(args.name)) {
@@ -432,7 +521,15 @@ function identityMatchesSubject(
432
521
  ))
433
522
  )
434
523
  return true;
435
- if (!sameKind(identity.kind, subject.kind)) return false;
524
+ // Naming an entry the listing holds is naming the listing, whatever kind
525
+ // the listing itself goes by.
526
+ const namesHeldEntry =
527
+ identity.listing &&
528
+ observation.data.type === "inventory" &&
529
+ observation.data.resources.some((resource) =>
530
+ listingRowNamesSubject(resource, subject),
531
+ );
532
+ if (!namesHeldEntry && !sameKind(identity.kind, subject.kind)) return false;
436
533
  // A subject with no name (a listing cited for what it does not contain) is
437
534
  // a wildcard that uniqueness still gates. A listing has no name of its own; the agent naming the entry it means
438
535
  // ("ConfigMap kube-root-ca.crt" in the ConfigMaps of a namespace) still
@@ -456,7 +553,19 @@ function identityMatchesSubject(
456
553
  ) {
457
554
  return false;
458
555
  }
556
+ // The object declaring a package lives where it lives, not in the
557
+ // listing's scope; every other held entry is named in the scope it has.
558
+ const namesDeclaredPackage =
559
+ namesHeldEntry &&
560
+ !sameKind(subject.kind, "Package") &&
561
+ observation.data.type === "inventory" &&
562
+ observation.data.resources.some(
563
+ (resource) =>
564
+ resource.kind === "Package" &&
565
+ listingRowNamesSubject(resource, subject),
566
+ );
459
567
  if (
568
+ !namesDeclaredPackage &&
460
569
  subject.namespace !== undefined &&
461
570
  identity.namespace !== undefined &&
462
571
  subject.namespace !== identity.namespace