kubeagent 0.1.48 → 0.1.49

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.
@@ -3,6 +3,7 @@ import readline from "node:readline";
3
3
  import { loadAuth } from "../auth.js";
4
4
  import { proxyRequest } from "../proxy-client.js";
5
5
  import { getLogs, describeResource, restartPod, rolloutRestart, scaleDeployment, getEvents, setResources, kubectlGetLogsSchema, kubectlDescribeSchema, restartPodSchema, rolloutRestartSchema, scaleDeploymentSchema, getEventsSchema, setResourcesSchema, } from "./tools.js";
6
+ import { z } from "zod";
6
7
  import { zodToJsonSchema } from "zod-to-json-schema";
7
8
  const MAX_TOOL_OUTPUT_CHARS = 8000;
8
9
  async function createMessage(params) {
@@ -17,7 +18,7 @@ function truncateOutput(output) {
17
18
  if (output.length <= MAX_TOOL_OUTPUT_CHARS)
18
19
  return output;
19
20
  const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
20
- return output.slice(0, half) + "\n\n... [truncated] ...\n\n" + output.slice(-half);
21
+ return (output.slice(0, half) + "\n\n... [truncated] ...\n\n" + output.slice(-half));
21
22
  }
22
23
  /**
23
24
  * Decide whether a tool call may execute, given the set of auto-safe actions and
@@ -41,12 +42,65 @@ export async function resolveApprovalDecision(toolName, params, safeActions, onA
41
42
  }
42
43
  const approved = await onApproval(toolName, params);
43
44
  if (!approved) {
44
- return { execute: false, reason: "Action denied by user. Propose an alternative." };
45
+ return {
46
+ execute: false,
47
+ reason: "Action denied by user. Propose an alternative.",
48
+ };
45
49
  }
46
50
  return { execute: true };
47
51
  }
52
+ const askUserQuestionSchema = z.object({
53
+ question: z.string(),
54
+ choices: z.array(z.string()).optional(),
55
+ });
56
+ const toolRegistry = [
57
+ {
58
+ name: "ask_user_question",
59
+ description: "Ask the user a clarifying question. Use when context is needed before diagnosing — e.g., a resource doesn't match any known deployment, or you need to know if an issue is intentional. Provide `choices` for multiple-choice questions, or omit for open-ended input.",
60
+ schema: askUserQuestionSchema,
61
+ },
62
+ {
63
+ name: "get_logs",
64
+ description: "Get logs from a pod. Use to investigate errors, crashes, OOM kills.",
65
+ schema: kubectlGetLogsSchema,
66
+ },
67
+ {
68
+ name: "describe_resource",
69
+ description: "Describe a Kubernetes resource (pod, deployment, service, node). Shows events, conditions, configuration.",
70
+ schema: kubectlDescribeSchema,
71
+ },
72
+ {
73
+ name: "restart_pod",
74
+ description: "Delete a pod. If the pod is owned by a Deployment or DaemonSet it will be recreated; orphan pods are permanently deleted. Requires user approval.",
75
+ schema: restartPodSchema,
76
+ },
77
+ {
78
+ name: "rollout_restart",
79
+ description: "Rollout restart a deployment. Requires user approval.",
80
+ schema: rolloutRestartSchema,
81
+ },
82
+ {
83
+ name: "scale_deployment",
84
+ description: "Scale a deployment to a specific number of replicas. Scaling to 0 requires approval.",
85
+ schema: scaleDeploymentSchema,
86
+ },
87
+ {
88
+ name: "get_events",
89
+ description: "Get recent events in a namespace. Useful for understanding what happened.",
90
+ schema: getEventsSchema,
91
+ },
92
+ {
93
+ name: "set_resources",
94
+ description: "Change CPU/memory requests and limits on a deployment. Triggers a rolling restart. Requires user approval.",
95
+ schema: setResourcesSchema,
96
+ },
97
+ ];
98
+ const toolInputSchemas = Object.fromEntries(toolRegistry.map(({ name, schema }) => [name, schema]));
48
99
  async function askUserQuestion(question, choices) {
49
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
100
+ const rl = readline.createInterface({
101
+ input: process.stdin,
102
+ output: process.stdout,
103
+ });
50
104
  return new Promise((resolve) => {
51
105
  let prompt = `\n${chalk.cyan("?")} ${chalk.bold(question)}\n`;
52
106
  if (choices && choices.length > 0) {
@@ -73,59 +127,11 @@ async function askUserQuestion(question, choices) {
73
127
  });
74
128
  }
75
129
  function makeToolDefs() {
76
- return [
77
- {
78
- name: "ask_user_question",
79
- description: "Ask the user a clarifying question. Use when context is needed before diagnosing — e.g., a resource doesn't match any known deployment, or you need to know if an issue is intentional. Provide `choices` for multiple-choice questions, or omit for open-ended input.",
80
- input_schema: {
81
- type: "object",
82
- properties: {
83
- question: { type: "string", description: "The question to ask the user" },
84
- choices: {
85
- type: "array",
86
- items: { type: "string" },
87
- description: "Optional list of choices. If provided, the user picks by number or types a free-form answer.",
88
- },
89
- },
90
- required: ["question"],
91
- },
92
- },
93
- {
94
- name: "get_logs",
95
- description: "Get logs from a pod. Use to investigate errors, crashes, OOM kills.",
96
- input_schema: zodToJsonSchema(kubectlGetLogsSchema),
97
- },
98
- {
99
- name: "describe_resource",
100
- description: "Describe a Kubernetes resource (pod, deployment, service, node). Shows events, conditions, configuration.",
101
- input_schema: zodToJsonSchema(kubectlDescribeSchema),
102
- },
103
- {
104
- name: "restart_pod",
105
- description: "Delete a pod. If the pod is owned by a Deployment or DaemonSet it will be recreated; orphan pods are permanently deleted. Requires user approval.",
106
- input_schema: zodToJsonSchema(restartPodSchema),
107
- },
108
- {
109
- name: "rollout_restart",
110
- description: "Rollout restart a deployment. Requires user approval.",
111
- input_schema: zodToJsonSchema(rolloutRestartSchema),
112
- },
113
- {
114
- name: "scale_deployment",
115
- description: "Scale a deployment to a specific number of replicas. Scaling to 0 requires approval.",
116
- input_schema: zodToJsonSchema(scaleDeploymentSchema),
117
- },
118
- {
119
- name: "get_events",
120
- description: "Get recent events in a namespace. Useful for understanding what happened.",
121
- input_schema: zodToJsonSchema(getEventsSchema),
122
- },
123
- {
124
- name: "set_resources",
125
- description: "Change CPU/memory requests and limits on a deployment. Triggers a rolling restart. Requires user approval.",
126
- input_schema: zodToJsonSchema(setResourcesSchema),
127
- },
128
- ];
130
+ return toolRegistry.map(({ name, description, schema }) => ({
131
+ name,
132
+ description,
133
+ input_schema: zodToJsonSchema(schema),
134
+ }));
129
135
  }
130
136
  function makeToolExecutors(noInteractive, onQuestion) {
131
137
  return {
@@ -168,7 +174,12 @@ If it requires a risky action (rollback, delete, scale to zero), propose it but
168
174
  { role: "user", content: userMessage },
169
175
  ];
170
176
  const toolExecutors = makeToolExecutors(options?.noInteractive ?? false, options?.onQuestion);
171
- const mutatingTools = new Set(["restart_pod", "rollout_restart", "scale_deployment", "set_resources"]);
177
+ const mutatingTools = new Set([
178
+ "restart_pod",
179
+ "rollout_restart",
180
+ "scale_deployment",
181
+ "set_resources",
182
+ ]);
172
183
  // Merge hardcoded read-only safe tools with user-configured safe actions
173
184
  const effectiveSafeActions = new Set([
174
185
  ...["get_logs", "get_events", "describe_resource", "ask_user_question"],
@@ -210,10 +221,24 @@ If it requires a risky action (rollback, delete, scale to zero), propose it but
210
221
  });
211
222
  continue;
212
223
  }
224
+ const parsedInput = toolInputSchemas[block.name]?.safeParse(block.input);
225
+ if (!parsedInput?.success) {
226
+ const details = parsedInput?.error.issues
227
+ .map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
228
+ .join("; ") ?? "no schema is registered";
229
+ toolResults.push({
230
+ type: "tool_result",
231
+ tool_use_id: block.id,
232
+ content: `Invalid input for tool ${block.name}: ${details}`,
233
+ is_error: true,
234
+ });
235
+ continue;
236
+ }
237
+ const validatedInput = parsedInput.data;
213
238
  // Check if action requires approval. A missing onApproval callback
214
239
  // (non-interactive mode) means DENY for any non-safe action — never
215
240
  // execute destructive remediation unattended.
216
- const decision = await resolveApprovalDecision(block.name, block.input, effectiveSafeActions, options?.onApproval);
241
+ const decision = await resolveApprovalDecision(block.name, validatedInput, effectiveSafeActions, options?.onApproval);
217
242
  if (!decision.execute) {
218
243
  toolResults.push({
219
244
  type: "tool_result",
@@ -223,13 +248,15 @@ If it requires a risky action (rollback, delete, scale to zero), propose it but
223
248
  continue;
224
249
  }
225
250
  try {
226
- const result = await executor(block.input, { context: clusterContext });
251
+ const result = await executor(validatedInput, {
252
+ context: clusterContext,
253
+ });
227
254
  // Track mutating actions for the verifier
228
255
  if (mutatingTools.has(block.name)) {
229
256
  appliedAction = {
230
257
  name: block.name,
231
258
  safe: effectiveSafeActions.has(block.name),
232
- params: block.input,
259
+ params: validatedInput,
233
260
  };
234
261
  }
235
262
  toolResults.push({
@@ -19,11 +19,37 @@ export function isActionSafe(action) {
19
19
  return safeActions.has(action);
20
20
  }
21
21
  // Tool input schemas for Claude API
22
+ const rfc1123LabelPattern = /^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/;
23
+ const namespaceNameSchema = z
24
+ .string()
25
+ .min(1)
26
+ .max(63)
27
+ .regex(rfc1123LabelPattern, "Must be a Kubernetes RFC 1123 label, not a command-line flag");
28
+ const kubernetesNameSchema = z
29
+ .string()
30
+ .min(1)
31
+ .max(253)
32
+ .regex(/^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/, "Must be a Kubernetes DNS subdomain, not a command-line flag")
33
+ .refine((name) => name
34
+ .split(".")
35
+ .every((label) => label.length <= 63 && rfc1123LabelPattern.test(label)), "Each Kubernetes DNS-subdomain label must be 1–63 characters");
36
+ const containerNameSchema = namespaceNameSchema;
37
+ const resourceQuantitySchema = z
38
+ .string()
39
+ .regex(/^\+?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+|[numkMGTPE]|[KMGTPE]i)?$/, "Must be a non-negative Kubernetes resource quantity");
22
40
  export const kubectlGetLogsSchema = z.object({
23
- namespace: z.string().describe("Kubernetes namespace"),
24
- pod: z.string().describe("Pod name"),
25
- container: z.string().optional().describe("Container name (if multi-container pod)"),
26
- tail: z.number().default(50).describe("Number of log lines to return"),
41
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
42
+ pod: kubernetesNameSchema.describe("Pod name"),
43
+ container: containerNameSchema
44
+ .optional()
45
+ .describe("Container name (if multi-container pod)"),
46
+ tail: z
47
+ .number()
48
+ .int()
49
+ .min(1)
50
+ .max(10_000)
51
+ .default(50)
52
+ .describe("Number of log lines to return"),
27
53
  });
28
54
  const ALLOWED_RESOURCE_TYPES = [
29
55
  "pod", "deployment", "service", "statefulset", "daemonset",
@@ -31,34 +57,44 @@ const ALLOWED_RESOURCE_TYPES = [
31
57
  "persistentvolumeclaim", "configmap", "endpoints",
32
58
  ];
33
59
  export const kubectlDescribeSchema = z.object({
34
- namespace: z.string().describe("Kubernetes namespace"),
60
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
35
61
  resource_type: z.enum(ALLOWED_RESOURCE_TYPES).describe("Resource type to describe"),
36
- name: z.string().describe("Resource name"),
62
+ name: kubernetesNameSchema.describe("Resource name"),
37
63
  });
38
64
  export const restartPodSchema = z.object({
39
- namespace: z.string().describe("Kubernetes namespace"),
40
- pod: z.string().describe("Pod name to delete (deployment will recreate it)"),
65
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
66
+ pod: kubernetesNameSchema.describe("Pod name to delete (deployment will recreate it)"),
41
67
  });
42
68
  export const rolloutRestartSchema = z.object({
43
- namespace: z.string().describe("Kubernetes namespace"),
44
- deployment: z.string().describe("Deployment name to rollout restart"),
69
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
70
+ deployment: kubernetesNameSchema.describe("Deployment name to rollout restart"),
45
71
  });
46
72
  export const scaleDeploymentSchema = z.object({
47
- namespace: z.string().describe("Kubernetes namespace"),
48
- deployment: z.string().describe("Deployment name"),
49
- replicas: z.number().min(0).max(20).describe("Target replica count"),
73
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
74
+ deployment: kubernetesNameSchema.describe("Deployment name"),
75
+ replicas: z.number().int().min(0).max(20).describe("Target replica count"),
50
76
  });
51
77
  export const getEventsSchema = z.object({
52
- namespace: z.string().describe("Kubernetes namespace"),
78
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
53
79
  });
54
80
  export const setResourcesSchema = z.object({
55
- namespace: z.string().describe("Kubernetes namespace"),
56
- deployment: z.string().describe("Deployment name"),
57
- container: z.string().optional().describe("Container name (omit for single-container pods)"),
58
- memory_request: z.string().optional().describe("Memory request e.g. '128Mi'"),
59
- memory_limit: z.string().optional().describe("Memory limit e.g. '512Mi'"),
60
- cpu_request: z.string().optional().describe("CPU request e.g. '100m'"),
61
- cpu_limit: z.string().optional().describe("CPU limit e.g. '500m'"),
81
+ namespace: namespaceNameSchema.describe("Kubernetes namespace"),
82
+ deployment: kubernetesNameSchema.describe("Deployment name"),
83
+ container: containerNameSchema
84
+ .optional()
85
+ .describe("Container name (omit for single-container pods)"),
86
+ memory_request: resourceQuantitySchema
87
+ .optional()
88
+ .describe("Memory request e.g. '128Mi'"),
89
+ memory_limit: resourceQuantitySchema
90
+ .optional()
91
+ .describe("Memory limit e.g. '512Mi'"),
92
+ cpu_request: resourceQuantitySchema
93
+ .optional()
94
+ .describe("CPU request e.g. '100m'"),
95
+ cpu_limit: resourceQuantitySchema
96
+ .optional()
97
+ .describe("CPU limit e.g. '500m'"),
62
98
  });
63
99
  // Tool implementations
64
100
  export async function getLogs(input, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubeagent",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "AI-powered Kubernetes management CLI",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://kubeagent.net",