kubeagent 0.1.35 → 0.1.43

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 (61) hide show
  1. package/README.md +34 -0
  2. package/dist/auth.js +15 -0
  3. package/dist/check/index.d.ts +34 -0
  4. package/dist/check/index.js +77 -0
  5. package/dist/check/index.test.d.ts +1 -0
  6. package/dist/check/index.test.js +202 -0
  7. package/dist/check/run.d.ts +10 -0
  8. package/dist/check/run.js +114 -0
  9. package/dist/cli.js +131 -0
  10. package/dist/detector/cache.d.ts +6 -0
  11. package/dist/detector/cache.js +44 -0
  12. package/dist/detector/cache.test.d.ts +1 -0
  13. package/dist/detector/cache.test.js +54 -0
  14. package/dist/detector/catalog.d.ts +17 -0
  15. package/dist/detector/catalog.js +88 -0
  16. package/dist/detector/catalog.test.d.ts +1 -0
  17. package/dist/detector/catalog.test.js +33 -0
  18. package/dist/detector/eol.d.ts +13 -0
  19. package/dist/detector/eol.js +64 -0
  20. package/dist/detector/eol.test.d.ts +1 -0
  21. package/dist/detector/eol.test.js +78 -0
  22. package/dist/detector/image-parser.d.ts +11 -0
  23. package/dist/detector/image-parser.js +59 -0
  24. package/dist/detector/image-parser.test.d.ts +1 -0
  25. package/dist/detector/image-parser.test.js +87 -0
  26. package/dist/detector/index.d.ts +33 -0
  27. package/dist/detector/index.js +217 -0
  28. package/dist/detector/index.test.d.ts +1 -0
  29. package/dist/detector/index.test.js +210 -0
  30. package/dist/detector/osv.d.ts +12 -0
  31. package/dist/detector/osv.js +50 -0
  32. package/dist/detector/osv.test.d.ts +1 -0
  33. package/dist/detector/osv.test.js +86 -0
  34. package/dist/detector/report.d.ts +6 -0
  35. package/dist/detector/report.js +91 -0
  36. package/dist/detector/sources.d.ts +55 -0
  37. package/dist/detector/sources.js +218 -0
  38. package/dist/detector/sources.test.d.ts +1 -0
  39. package/dist/detector/sources.test.js +36 -0
  40. package/dist/diagnoser/approval.test.d.ts +1 -0
  41. package/dist/diagnoser/approval.test.js +49 -0
  42. package/dist/diagnoser/index.d.ts +16 -0
  43. package/dist/diagnoser/index.js +37 -11
  44. package/dist/diagnoser/tools.d.ts +2 -2
  45. package/dist/kb/loader.js +6 -0
  46. package/dist/kb/writer.d.ts +1 -0
  47. package/dist/kb/writer.js +4 -0
  48. package/dist/monitor/index.d.ts +11 -1
  49. package/dist/monitor/index.js +45 -14
  50. package/dist/monitor/scope.test.d.ts +1 -0
  51. package/dist/monitor/scope.test.js +87 -0
  52. package/dist/notify/discord.js +6 -9
  53. package/dist/notify/ssrf-guard.d.ts +5 -0
  54. package/dist/notify/ssrf-guard.js +150 -0
  55. package/dist/notify/teams.js +6 -9
  56. package/dist/notify/webhook.js +7 -9
  57. package/dist/notify/webhook.test.js +21 -0
  58. package/dist/onboard/index.js +23 -1
  59. package/dist/orchestrator.js +2 -1
  60. package/dist/telemetry.js +27 -1
  61. package/package.json +10 -1
@@ -0,0 +1,55 @@
1
+ import { type KubectlOptions } from "../kubectl.js";
2
+ export type WorkloadKind = "Deployment" | "StatefulSet" | "DaemonSet";
3
+ export interface WorkloadRef {
4
+ kind: WorkloadKind;
5
+ namespace: string;
6
+ name: string;
7
+ hosts?: string[];
8
+ }
9
+ export declare function workloadKey(ref: {
10
+ kind: string;
11
+ namespace: string;
12
+ name: string;
13
+ }): string;
14
+ export interface ImageCandidate {
15
+ source: "image";
16
+ image: string;
17
+ workload: WorkloadRef;
18
+ container: string;
19
+ }
20
+ export interface LabelCandidate {
21
+ source: "label";
22
+ appName: string;
23
+ appVersion: string;
24
+ workload: WorkloadRef;
25
+ }
26
+ export interface HelmCandidate {
27
+ source: "helm";
28
+ chart: string;
29
+ chartVersion: string;
30
+ appVersion?: string;
31
+ namespace: string;
32
+ releaseName: string;
33
+ }
34
+ export type Candidate = ImageCandidate | LabelCandidate | HelmCandidate;
35
+ export declare function fetchWorkloadCandidates(options: KubectlOptions): Promise<Candidate[]>;
36
+ interface HelmReleasePayload {
37
+ name: string;
38
+ namespace: string;
39
+ chart?: {
40
+ metadata?: {
41
+ name?: string;
42
+ version?: string;
43
+ appVersion?: string;
44
+ };
45
+ };
46
+ }
47
+ export declare function decodeHelmRelease(b64: string): HelmReleasePayload | null;
48
+ export declare function fetchHelmCandidates(options: KubectlOptions): Promise<HelmCandidate[]>;
49
+ /**
50
+ * Build a map of workload key (Kind/namespace/name) → ingress hostnames.
51
+ * Joins Ingresses → Services → workload pod-template labels best-effort.
52
+ * Returns an empty map on failure (ingress visibility is non-essential).
53
+ */
54
+ export declare function fetchIngressHostMap(options: KubectlOptions): Promise<Map<string, string[]>>;
55
+ export {};
@@ -0,0 +1,218 @@
1
+ import { gunzipSync } from "node:zlib";
2
+ import { Buffer } from "node:buffer";
3
+ import { kubectlJson } from "../kubectl.js";
4
+ import { dbg } from "../debug.js";
5
+ export function workloadKey(ref) {
6
+ return `${ref.kind}/${ref.namespace}/${ref.name}`;
7
+ }
8
+ const LABEL_NAME = "app.kubernetes.io/name";
9
+ const LABEL_VERSION = "app.kubernetes.io/version";
10
+ async function fetchWorkload(kind, resource, options) {
11
+ return (await kubectlJson(["get", resource, "--all-namespaces"], options));
12
+ }
13
+ function extractFromWorkload(kind, w) {
14
+ const out = [];
15
+ const ref = { kind, namespace: w.metadata.namespace, name: w.metadata.name };
16
+ const containers = [
17
+ ...(w.spec?.template?.spec?.containers ?? []),
18
+ ...(w.spec?.template?.spec?.initContainers ?? []),
19
+ ];
20
+ for (const c of containers) {
21
+ if (c.image) {
22
+ out.push({ source: "image", image: c.image, workload: ref, container: c.name });
23
+ }
24
+ }
25
+ const labels = {
26
+ ...(w.metadata.labels ?? {}),
27
+ ...(w.spec?.template?.metadata?.labels ?? {}),
28
+ };
29
+ if (labels[LABEL_NAME] && labels[LABEL_VERSION]) {
30
+ out.push({
31
+ source: "label",
32
+ appName: labels[LABEL_NAME],
33
+ appVersion: labels[LABEL_VERSION],
34
+ workload: ref,
35
+ });
36
+ }
37
+ return out;
38
+ }
39
+ export async function fetchWorkloadCandidates(options) {
40
+ const [deploys, statefulsets, daemonsets] = await Promise.all([
41
+ fetchWorkload("Deployment", "deployments", options),
42
+ fetchWorkload("StatefulSet", "statefulsets", options),
43
+ fetchWorkload("DaemonSet", "daemonsets", options),
44
+ ]);
45
+ const all = [];
46
+ for (const w of deploys.items)
47
+ all.push(...extractFromWorkload("Deployment", w));
48
+ for (const w of statefulsets.items)
49
+ all.push(...extractFromWorkload("StatefulSet", w));
50
+ for (const w of daemonsets.items)
51
+ all.push(...extractFromWorkload("DaemonSet", w));
52
+ return all;
53
+ }
54
+ export function decodeHelmRelease(b64) {
55
+ try {
56
+ // Helm v3 stores releases as base64(gzip(json)). When fetched via
57
+ // `kubectl get secrets -o json`, the API server applies its own base64
58
+ // layer on top of `data.release`, so we decode twice before gunzip.
59
+ const outer = Buffer.from(b64, "base64");
60
+ const inner = Buffer.from(outer.toString("utf-8"), "base64");
61
+ const json = gunzipSync(inner).toString("utf-8");
62
+ return JSON.parse(json);
63
+ }
64
+ catch (err) {
65
+ dbg("detector", `helm release decode failed: ${err.message}`);
66
+ return null;
67
+ }
68
+ }
69
+ export async function fetchHelmCandidates(options) {
70
+ let list;
71
+ try {
72
+ list = (await kubectlJson(["get", "secrets", "--all-namespaces", "-l", "owner=helm,status=deployed"], options));
73
+ }
74
+ catch (err) {
75
+ dbg("detector", `helm secret listing failed: ${err.message}`);
76
+ return [];
77
+ }
78
+ const seen = new Set();
79
+ const out = [];
80
+ for (const s of list.items) {
81
+ const release = s.data?.release;
82
+ if (!release)
83
+ continue;
84
+ const payload = decodeHelmRelease(release);
85
+ if (!payload)
86
+ continue;
87
+ const chart = payload.chart?.metadata?.name;
88
+ const version = payload.chart?.metadata?.version;
89
+ if (!chart || !version)
90
+ continue;
91
+ const key = `${s.metadata.namespace}/${payload.name}/${version}`;
92
+ if (seen.has(key))
93
+ continue;
94
+ seen.add(key);
95
+ out.push({
96
+ source: "helm",
97
+ chart,
98
+ chartVersion: version,
99
+ appVersion: payload.chart?.metadata?.appVersion,
100
+ namespace: s.metadata.namespace,
101
+ releaseName: payload.name,
102
+ });
103
+ }
104
+ return out;
105
+ }
106
+ function selectorMatches(selector, labels) {
107
+ const keys = Object.keys(selector);
108
+ if (keys.length === 0)
109
+ return false;
110
+ for (const k of keys) {
111
+ if (labels[k] !== selector[k])
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+ /**
117
+ * Build a map of workload key (Kind/namespace/name) → ingress hostnames.
118
+ * Joins Ingresses → Services → workload pod-template labels best-effort.
119
+ * Returns an empty map on failure (ingress visibility is non-essential).
120
+ */
121
+ export async function fetchIngressHostMap(options) {
122
+ const out = new Map();
123
+ let services;
124
+ let ingresses;
125
+ let workloadLists;
126
+ try {
127
+ [services, ingresses, ...workloadLists] = await Promise.all([
128
+ kubectlJson(["get", "services", "--all-namespaces"], options),
129
+ kubectlJson(["get", "ingresses", "--all-namespaces"], options),
130
+ fetchWorkload("Deployment", "deployments", options),
131
+ fetchWorkload("StatefulSet", "statefulsets", options),
132
+ fetchWorkload("DaemonSet", "daemonsets", options),
133
+ ]);
134
+ }
135
+ catch (err) {
136
+ dbg("detector", `ingress map fetch failed: ${err.message}`);
137
+ return out;
138
+ }
139
+ // service ns/name → selector
140
+ const serviceSelectors = new Map();
141
+ for (const svc of services.items) {
142
+ const sel = svc.spec?.selector;
143
+ if (!sel || Object.keys(sel).length === 0)
144
+ continue;
145
+ serviceSelectors.set(`${svc.metadata.namespace}/${svc.metadata.name}`, sel);
146
+ }
147
+ // service ns/name → set of hosts
148
+ const serviceHosts = new Map();
149
+ const addServiceHost = (ns, svc, host) => {
150
+ if (!svc || !host)
151
+ return;
152
+ const key = `${ns}/${svc}`;
153
+ let s = serviceHosts.get(key);
154
+ if (!s) {
155
+ s = new Set();
156
+ serviceHosts.set(key, s);
157
+ }
158
+ s.add(host);
159
+ };
160
+ for (const ing of ingresses.items) {
161
+ const ns = ing.metadata.namespace;
162
+ const tlsHosts = new Set();
163
+ for (const t of ing.spec?.tls ?? []) {
164
+ for (const h of t.hosts ?? [])
165
+ tlsHosts.add(h);
166
+ }
167
+ for (const rule of ing.spec?.rules ?? []) {
168
+ // A rule with no `host` matches all inbound hosts per the Ingress
169
+ // spec — the backend is still publicly reachable, so record it as `*`
170
+ // rather than dropping it (which would mis-classify as internal-only).
171
+ const host = rule.host ?? "*";
172
+ for (const p of rule.http?.paths ?? []) {
173
+ const svc = p.backend?.service?.name ?? p.backend?.serviceName;
174
+ addServiceHost(ns, svc, host);
175
+ }
176
+ }
177
+ const defSvc = ing.spec?.defaultBackend?.service?.name ?? ing.spec?.defaultBackend?.serviceName;
178
+ if (defSvc) {
179
+ // defaultBackend catches anything not matched by a rule — also wildcard
180
+ // exposure. Prefer explicit TLS hosts when present for better display.
181
+ if (tlsHosts.size > 0) {
182
+ for (const h of tlsHosts)
183
+ addServiceHost(ns, defSvc, h);
184
+ }
185
+ else {
186
+ addServiceHost(ns, defSvc, "*");
187
+ }
188
+ }
189
+ }
190
+ const allWorkloads = [];
191
+ const kinds = ["Deployment", "StatefulSet", "DaemonSet"];
192
+ workloadLists.forEach((list, idx) => {
193
+ for (const item of list.items)
194
+ allWorkloads.push({ kind: kinds[idx], item });
195
+ });
196
+ for (const { kind, item } of allWorkloads) {
197
+ const ns = item.metadata.namespace;
198
+ const podLabels = item.spec?.template?.metadata?.labels ?? {};
199
+ if (Object.keys(podLabels).length === 0)
200
+ continue;
201
+ const hosts = new Set();
202
+ for (const [svcKey, sel] of serviceSelectors) {
203
+ if (!svcKey.startsWith(`${ns}/`))
204
+ continue;
205
+ if (!selectorMatches(sel, podLabels))
206
+ continue;
207
+ const hs = serviceHosts.get(svcKey);
208
+ if (!hs)
209
+ continue;
210
+ for (const h of hs)
211
+ hosts.add(h);
212
+ }
213
+ if (hosts.size > 0) {
214
+ out.set(workloadKey({ kind, namespace: ns, name: item.metadata.name }), [...hosts].sort());
215
+ }
216
+ }
217
+ return out;
218
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { gzipSync } from "node:zlib";
3
+ import { Buffer } from "node:buffer";
4
+ import { decodeHelmRelease } from "./sources.js";
5
+ // Reproduce the encoding chain Helm + the k8s API actually produce.
6
+ // Helm stores the release as `base64(gzip(json))` inside the Secret value,
7
+ // and the k8s API base64-encodes Secret `data.*` values on the wire — so the
8
+ // string kubectl returns is `base64(base64(gzip(json)))`. This test exists
9
+ // to lock in the decoder's behaviour against that chain so we can't
10
+ // regress to a single-base64 decode (which silently drops every Helm
11
+ // release at runtime).
12
+ function encodeAsKubectlReturnsIt(payload) {
13
+ const json = JSON.stringify(payload);
14
+ const gzipped = gzipSync(Buffer.from(json, "utf-8"));
15
+ const helmLayer = gzipped.toString("base64");
16
+ const apiLayer = Buffer.from(helmLayer, "utf-8").toString("base64");
17
+ return apiLayer;
18
+ }
19
+ describe("decodeHelmRelease", () => {
20
+ it("decodes a release blob in the format kubectl returns", () => {
21
+ const release = {
22
+ name: "postgres",
23
+ namespace: "data",
24
+ chart: { metadata: { name: "postgresql", version: "12.0.0", appVersion: "15.2.0" } },
25
+ };
26
+ const encoded = encodeAsKubectlReturnsIt(release);
27
+ expect(decodeHelmRelease(encoded)).toEqual(release);
28
+ });
29
+ it("returns null on garbage input rather than throwing", () => {
30
+ expect(decodeHelmRelease("not-base64-and-not-gzip")).toBeNull();
31
+ });
32
+ it("returns null on valid base64 that isn't gzip", () => {
33
+ const apiLayer = Buffer.from("aGVsbG8gd29ybGQ=", "utf-8").toString("base64");
34
+ expect(decodeHelmRelease(apiLayer)).toBeNull();
35
+ });
36
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { resolveApprovalDecision } from "./index.js";
3
+ const SAFE = new Set(["get_logs", "get_events", "describe_resource", "ask_user_question"]);
4
+ describe("resolveApprovalDecision", () => {
5
+ it("auto-executes a safe (read-only) action with no approver", async () => {
6
+ const decision = await resolveApprovalDecision("get_logs", {}, SAFE, undefined);
7
+ expect(decision.execute).toBe(true);
8
+ });
9
+ it("DENIES a non-safe mutating action when no approver exists (non-interactive mode)", async () => {
10
+ const decision = await resolveApprovalDecision("scale_deployment", { deployment: "web", replicas: 0 }, SAFE, undefined);
11
+ expect(decision.execute).toBe(false);
12
+ if (!decision.execute) {
13
+ expect(decision.reason).toMatch(/no approver is available/i);
14
+ }
15
+ });
16
+ it("never invokes an executor implicitly — caller must gate on execute=false", async () => {
17
+ // Simulates the tool loop: a denied decision must not run the executor.
18
+ const executor = vi.fn();
19
+ const decision = await resolveApprovalDecision("restart_pod", { pod: "x" }, SAFE, undefined);
20
+ if (decision.execute)
21
+ executor();
22
+ expect(executor).not.toHaveBeenCalled();
23
+ });
24
+ it("calls onApproval for non-safe actions and executes when approved", async () => {
25
+ const onApproval = vi.fn().mockResolvedValue(true);
26
+ const decision = await resolveApprovalDecision("scale_deployment", { deployment: "web", replicas: 3 }, SAFE, onApproval);
27
+ expect(onApproval).toHaveBeenCalledWith("scale_deployment", { deployment: "web", replicas: 3 });
28
+ expect(decision.execute).toBe(true);
29
+ });
30
+ it("denies with the user-denial message when onApproval returns false", async () => {
31
+ const onApproval = vi.fn().mockResolvedValue(false);
32
+ const decision = await resolveApprovalDecision("restart_pod", {}, SAFE, onApproval);
33
+ expect(decision.execute).toBe(false);
34
+ if (!decision.execute) {
35
+ expect(decision.reason).toMatch(/denied by user/i);
36
+ }
37
+ });
38
+ it("does not call onApproval for safe actions", async () => {
39
+ const onApproval = vi.fn().mockResolvedValue(false);
40
+ const decision = await resolveApprovalDecision("get_logs", {}, SAFE, onApproval);
41
+ expect(onApproval).not.toHaveBeenCalled();
42
+ expect(decision.execute).toBe(true);
43
+ });
44
+ it("treats user-configured safe_actions as auto-executable", async () => {
45
+ const safe = new Set([...SAFE, "rollout_restart"]);
46
+ const decision = await resolveApprovalDecision("rollout_restart", {}, safe, undefined);
47
+ expect(decision.execute).toBe(true);
48
+ });
49
+ });
@@ -1,4 +1,20 @@
1
1
  import type { Issue } from "../monitor/types.js";
2
+ export type ApprovalDecision = {
3
+ execute: true;
4
+ } | {
5
+ execute: false;
6
+ reason: string;
7
+ };
8
+ /**
9
+ * Decide whether a tool call may execute, given the set of auto-safe actions and
10
+ * the (optional) approval callback.
11
+ *
12
+ * Security-critical: a missing/undefined `onApproval` MUST mean DENY for any
13
+ * action that is not in `safeActions`. In non-interactive mode the CLI passes no
14
+ * (or a deny-all) approver, and destructive remediation (e.g. scale-to-0) must
15
+ * never run unattended.
16
+ */
17
+ export declare function resolveApprovalDecision(toolName: string, params: Record<string, unknown>, safeActions: Set<string>, onApproval?: (action: string, params: Record<string, unknown>) => Promise<boolean>): Promise<ApprovalDecision>;
2
18
  export interface DiagnosisResult {
3
19
  analysis: string;
4
20
  action?: {
@@ -19,6 +19,32 @@ function truncateOutput(output) {
19
19
  const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
20
20
  return output.slice(0, half) + "\n\n... [truncated] ...\n\n" + output.slice(-half);
21
21
  }
22
+ /**
23
+ * Decide whether a tool call may execute, given the set of auto-safe actions and
24
+ * the (optional) approval callback.
25
+ *
26
+ * Security-critical: a missing/undefined `onApproval` MUST mean DENY for any
27
+ * action that is not in `safeActions`. In non-interactive mode the CLI passes no
28
+ * (or a deny-all) approver, and destructive remediation (e.g. scale-to-0) must
29
+ * never run unattended.
30
+ */
31
+ export async function resolveApprovalDecision(toolName, params, safeActions, onApproval) {
32
+ // Safe (read-only or explicitly allowlisted) actions auto-execute.
33
+ if (safeActions.has(toolName))
34
+ return { execute: true };
35
+ // Non-safe action requires approval regardless of whether a callback exists.
36
+ if (!onApproval) {
37
+ return {
38
+ execute: false,
39
+ reason: "Action requires approval but no approver is available (non-interactive mode); denied. Propose a safe alternative or escalate to a human.",
40
+ };
41
+ }
42
+ const approved = await onApproval(toolName, params);
43
+ if (!approved) {
44
+ return { execute: false, reason: "Action denied by user. Propose an alternative." };
45
+ }
46
+ return { execute: true };
47
+ }
22
48
  async function askUserQuestion(question, choices) {
23
49
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
24
50
  return new Promise((resolve) => {
@@ -184,17 +210,17 @@ If it requires a risky action (rollback, delete, scale to zero), propose it but
184
210
  });
185
211
  continue;
186
212
  }
187
- // Check if action requires approval
188
- if (!effectiveSafeActions.has(block.name) && options?.onApproval) {
189
- const approved = await options.onApproval(block.name, block.input);
190
- if (!approved) {
191
- toolResults.push({
192
- type: "tool_result",
193
- tool_use_id: block.id,
194
- content: "Action denied by user. Propose an alternative.",
195
- });
196
- continue;
197
- }
213
+ // Check if action requires approval. A missing onApproval callback
214
+ // (non-interactive mode) means DENY for any non-safe action — never
215
+ // execute destructive remediation unattended.
216
+ const decision = await resolveApprovalDecision(block.name, block.input, effectiveSafeActions, options?.onApproval);
217
+ if (!decision.execute) {
218
+ toolResults.push({
219
+ type: "tool_result",
220
+ tool_use_id: block.id,
221
+ content: decision.reason,
222
+ });
223
+ continue;
198
224
  }
199
225
  try {
200
226
  const result = await executor(block.input, { context: clusterContext });
@@ -25,11 +25,11 @@ export declare const kubectlDescribeSchema: z.ZodObject<{
25
25
  }, "strip", z.ZodTypeAny, {
26
26
  name: string;
27
27
  namespace: string;
28
- resource_type: "pod" | "deployment" | "service" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "node" | "persistentvolumeclaim" | "configmap" | "endpoints";
28
+ resource_type: "service" | "node" | "pod" | "deployment" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "persistentvolumeclaim" | "configmap" | "endpoints";
29
29
  }, {
30
30
  name: string;
31
31
  namespace: string;
32
- resource_type: "pod" | "deployment" | "service" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "node" | "persistentvolumeclaim" | "configmap" | "endpoints";
32
+ resource_type: "service" | "node" | "pod" | "deployment" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "persistentvolumeclaim" | "configmap" | "endpoints";
33
33
  }>;
34
34
  export declare const restartPodSchema: z.ZodObject<{
35
35
  namespace: z.ZodString;
package/dist/kb/loader.js CHANGED
@@ -13,6 +13,12 @@ export function buildSystemPrompt(kbDir) {
13
13
  sections.push(readFileSync(clusterPath, "utf-8"));
14
14
  sections.push("");
15
15
  }
16
+ // Load applications.md (OSS app inventory with CVE/EOL status)
17
+ const applicationsPath = join(kbDir, "applications.md");
18
+ if (existsSync(applicationsPath)) {
19
+ sections.push(readFileSync(applicationsPath, "utf-8"));
20
+ sections.push("");
21
+ }
16
22
  // Load project files
17
23
  const projectsDir = join(kbDir, "projects");
18
24
  if (existsSync(projectsDir)) {
@@ -1,5 +1,6 @@
1
1
  export declare function ensureKbDir(kbDir: string): void;
2
2
  export declare function writeClusterKb(kbDir: string, content: string): void;
3
+ export declare function writeApplicationsKb(kbDir: string, content: string): void;
3
4
  export declare function writeProjectKb(kbDir: string, projectName: string, content: string): void;
4
5
  export declare function writeRunbook(kbDir: string, name: string, content: string): void;
5
6
  /**
package/dist/kb/writer.js CHANGED
@@ -12,6 +12,10 @@ export function writeClusterKb(kbDir, content) {
12
12
  ensureKbDir(kbDir);
13
13
  writeFileSync(join(kbDir, "cluster.md"), content);
14
14
  }
15
+ export function writeApplicationsKb(kbDir, content) {
16
+ ensureKbDir(kbDir);
17
+ writeFileSync(join(kbDir, "applications.md"), content);
18
+ }
15
19
  export function writeProjectKb(kbDir, projectName, content) {
16
20
  ensureKbDir(kbDir);
17
21
  writeFileSync(join(kbDir, "projects", `${projectName}.md`), content);
@@ -8,8 +8,18 @@ export interface CheckSummary {
8
8
  nodeCount: number;
9
9
  namespaceCount: number;
10
10
  }
11
+ export interface RunChecksScope {
12
+ /**
13
+ * Namespaces to scope the scan to. When set, kubectl queries are issued
14
+ * per-namespace (e.g. `kubectl get pods -n foo`) instead of the
15
+ * cluster-wide `--all-namespaces` form. Cluster-scoped queries
16
+ * (`get nodes`, `get namespaces`) are still attempted but their failures
17
+ * are tolerated so namespace-scoped service accounts can use `kubeagent check`.
18
+ */
19
+ namespaces?: string[];
20
+ }
11
21
  export declare function runChecks(options: KubectlOptions): Promise<Issue[]>;
12
- export declare function runChecks(options: KubectlOptions, withSummary: true): Promise<CheckSummary>;
22
+ export declare function runChecks(options: KubectlOptions, withSummary: true, scope?: RunChecksScope): Promise<CheckSummary>;
13
23
  export declare function computeResolvedKeys(activeKeys: Set<string>, currentKeys: Set<string>): string[];
14
24
  export declare function updateActiveKeys(activeKeys: Set<string>, resolvedKeys: string[], currentKeys: Set<string>): void;
15
25
  export declare function startMonitor(options: KubectlOptions, intervalMs: number, onIssues: IssueCallback, onResolved?: ResolveCallback, onFirstCycle?: () => void): {
@@ -33,28 +33,59 @@ function deduplicateIssues(issues) {
33
33
  return true;
34
34
  });
35
35
  }
36
- export async function runChecks(options, withSummary) {
36
+ export async function runChecks(options, withSummary, scope) {
37
37
  const allIssues = [];
38
- // Get all pods across namespaces
39
- const podList = (await kubectlJson(["get", "pods", "--all-namespaces"], options));
40
- allIssues.push(...findPodIssues(podList));
41
- // Get nodes
42
- const nodeList = (await kubectlJson(["get", "nodes"], options));
38
+ const scoped = scope?.namespaces && scope.namespaces.length > 0 ? scope.namespaces : null;
39
+ // Pods: query per-namespace when scoped (so ns-bound service accounts
40
+ // don't need cluster-wide list-pods), otherwise all-namespaces.
41
+ let podCount = 0;
42
+ if (scoped) {
43
+ for (const ns of scoped) {
44
+ const list = (await kubectlJson(["get", "pods", "-n", ns], options).catch(() => ({ items: [] })));
45
+ allIssues.push(...findPodIssues(list));
46
+ podCount += list.items.length;
47
+ }
48
+ }
49
+ else {
50
+ const podList = (await kubectlJson(["get", "pods", "--all-namespaces"], options));
51
+ allIssues.push(...findPodIssues(podList));
52
+ podCount = podList.items.length;
53
+ }
54
+ // Nodes: cluster-scoped. Tolerate forbidden so ns-bound service accounts
55
+ // still get a useful pod-level report (node issues just don't apply to them).
56
+ const nodeList = (await kubectlJson(["get", "nodes"], options).catch(() => ({ items: [] })));
43
57
  allIssues.push(...findNodeIssues(nodeList));
44
- // Get failed jobs
45
- const jobList = (await kubectlJson(["get", "jobs", "--all-namespaces", "--field-selector=status.failed>0"], options).catch(() => ({ items: [] })));
46
- allIssues.push(...findJobIssues(jobList));
58
+ // Failed jobs: per-namespace when scoped, otherwise all-namespaces.
59
+ if (scoped) {
60
+ for (const ns of scoped) {
61
+ const list = (await kubectlJson(["get", "jobs", "-n", ns, "--field-selector=status.failed>0"], options).catch(() => ({ items: [] })));
62
+ allIssues.push(...findJobIssues(list));
63
+ }
64
+ }
65
+ else {
66
+ const jobList = (await kubectlJson(["get", "jobs", "--all-namespaces", "--field-selector=status.failed>0"], options).catch(() => ({ items: [] })));
67
+ allIssues.push(...findJobIssues(jobList));
68
+ }
47
69
  const issues = deduplicateIssues(allIssues);
48
70
  if (!withSummary)
49
71
  return issues;
50
- // Fetch namespace count (non-fatal)
51
- const nsList = await kubectlJson(["get", "namespaces"], options)
52
- .catch(() => ({ items: [] }));
72
+ // Namespace count: cluster-scoped, tolerate forbidden the same way as nodes.
73
+ // When the caller already specified namespaces, use that length so the
74
+ // summary reflects the requested scope, not the cluster's namespace total.
75
+ let namespaceCount;
76
+ if (scoped) {
77
+ namespaceCount = scoped.length;
78
+ }
79
+ else {
80
+ const nsList = await kubectlJson(["get", "namespaces"], options)
81
+ .catch(() => ({ items: [] }));
82
+ namespaceCount = nsList.items.length;
83
+ }
53
84
  return {
54
85
  issues,
55
- podCount: podList.items.length,
86
+ podCount,
56
87
  nodeCount: nodeList.items.length,
57
- namespaceCount: nsList.items.length,
88
+ namespaceCount,
58
89
  };
59
90
  }
60
91
  // How long a pod must be Pending before it's reported as an issue.
@@ -0,0 +1 @@
1
+ export {};