@skyhook-io/radar-app 1.14.2 → 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.
Files changed (74) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +30 -0
  3. package/src/components/CloudConnectFlow.tsx +21 -7
  4. package/src/components/CloudFunnelButton.tsx +85 -8
  5. package/src/components/cloudConnectHandoff.test.ts +10 -1
  6. package/src/components/cloudConnectHandoff.ts +15 -1
  7. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  8. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  9. package/src/components/diagnose/AgentControls.tsx +603 -0
  10. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  11. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  12. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  13. package/src/components/diagnose/AssessmentCard.tsx +1560 -0
  14. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  15. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  16. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  17. package/src/components/diagnose/Home.test.tsx +1 -3
  18. package/src/components/diagnose/Home.tsx +2 -1
  19. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  20. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  21. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  22. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  23. package/src/components/diagnose/InvestigationView.tsx +11 -28
  24. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  25. package/src/components/diagnose/assessmentCopy.ts +169 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +631 -2
  27. package/src/components/diagnose/investigationCase.ts +132 -24
  28. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  31. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  38. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  39. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  40. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  41. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  48. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  49. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  50. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  51. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  52. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  53. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  54. package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
  55. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  57. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  58. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  59. package/src/components/diagnose/parts.test.tsx +3 -1
  60. package/src/components/diagnose/parts.tsx +26 -3904
  61. package/src/components/diagnose/target.ts +9 -3
  62. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  63. package/src/components/diagnose/toolCallLabel.ts +41 -2
  64. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  65. package/src/components/home/MCPSetupDialog.tsx +1 -1
  66. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  67. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  68. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  69. package/src/components/resources/PodFilePreview.tsx +1 -1
  70. package/src/components/ui/CommandPalette.tsx +12 -8
  71. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  72. package/src/components/ui/Omnibar.tsx +33 -10
  73. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  74. 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,
@@ -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,75 @@ 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
391
  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;
392
+ if (
393
+ kind !== observation.data.type &&
394
+ !(kind === "resource" && RESOURCE_SHAPED.has(observation.data.type))
395
+ )
396
+ return false;
339
397
  if (
340
398
  qualifier !== undefined &&
341
399
  observation.data.type === "metrics" &&
@@ -371,6 +429,36 @@ function observationIdentities(
371
429
  ): ObservationSubjectIdentity[] {
372
430
  const stated = observationSubjectIdentity(observation);
373
431
  const identities = stated ? [stated] : [];
432
+ // A ranking is also each row it ranks and the workload each pod belongs
433
+ // to; a posture card each resource its findings name and the workload that
434
+ // owns it: a citation of the workload reaches its pods' rows and the
435
+ // finding on its HPA, and a card of mixed kinds accepts no name it does
436
+ // not hold.
437
+ if (
438
+ observation.data.type === "ranking" ||
439
+ observation.data.type === "posture"
440
+ ) {
441
+ const rows =
442
+ observation.data.type === "ranking"
443
+ ? observation.data.rows.flatMap((row) =>
444
+ row.owner ? [row, row.owner] : [row],
445
+ )
446
+ : observation.data.findings.flatMap((finding) =>
447
+ finding.managedBy ? [finding, finding.managedBy] : [finding],
448
+ );
449
+ const seen = new Set<string>();
450
+ for (const row of rows) {
451
+ const key = `${row.kind}/${row.namespace ?? ""}/${row.name}`;
452
+ if (seen.has(key)) continue;
453
+ seen.add(key);
454
+ identities.push({
455
+ kind: row.kind,
456
+ group: "group" in row ? row.group : undefined,
457
+ namespace: row.namespace ?? "",
458
+ name: row.name,
459
+ });
460
+ }
461
+ }
374
462
  if (observation.data.type === "logs" && stated) {
375
463
  const args = investigationSourceArgs(observation.source);
376
464
  if (args && nonEmptyString(args.kind) && nonEmptyString(args.name)) {
@@ -432,7 +520,15 @@ function identityMatchesSubject(
432
520
  ))
433
521
  )
434
522
  return true;
435
- if (!sameKind(identity.kind, subject.kind)) return false;
523
+ // Naming an entry the listing holds is naming the listing, whatever kind
524
+ // the listing itself goes by.
525
+ const namesHeldEntry =
526
+ identity.listing &&
527
+ observation.data.type === "inventory" &&
528
+ observation.data.resources.some((resource) =>
529
+ listingRowNamesSubject(resource, subject),
530
+ );
531
+ if (!namesHeldEntry && !sameKind(identity.kind, subject.kind)) return false;
436
532
  // A subject with no name (a listing cited for what it does not contain) is
437
533
  // a wildcard that uniqueness still gates. A listing has no name of its own; the agent naming the entry it means
438
534
  // ("ConfigMap kube-root-ca.crt" in the ConfigMaps of a namespace) still
@@ -456,7 +552,19 @@ function identityMatchesSubject(
456
552
  ) {
457
553
  return false;
458
554
  }
555
+ // The object declaring a package lives where it lives, not in the
556
+ // listing's scope; every other held entry is named in the scope it has.
557
+ const namesDeclaredPackage =
558
+ namesHeldEntry &&
559
+ !sameKind(subject.kind, "Package") &&
560
+ observation.data.type === "inventory" &&
561
+ observation.data.resources.some(
562
+ (resource) =>
563
+ resource.kind === "Package" &&
564
+ listingRowNamesSubject(resource, subject),
565
+ );
459
566
  if (
567
+ !namesDeclaredPackage &&
460
568
  subject.namespace !== undefined &&
461
569
  identity.namespace !== undefined &&
462
570
  subject.namespace !== identity.namespace