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.
- package/dist/diagnoser/index.js +87 -60
- package/dist/diagnoser/tools.js +57 -21
- package/package.json +1 -1
package/dist/diagnoser/index.js
CHANGED
|
@@ -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 {
|
|
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({
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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([
|
|
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,
|
|
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(
|
|
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:
|
|
259
|
+
params: validatedInput,
|
|
233
260
|
};
|
|
234
261
|
}
|
|
235
262
|
toolResults.push({
|
package/dist/diagnoser/tools.js
CHANGED
|
@@ -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:
|
|
24
|
-
pod:
|
|
25
|
-
container:
|
|
26
|
-
|
|
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:
|
|
60
|
+
namespace: namespaceNameSchema.describe("Kubernetes namespace"),
|
|
35
61
|
resource_type: z.enum(ALLOWED_RESOURCE_TYPES).describe("Resource type to describe"),
|
|
36
|
-
name:
|
|
62
|
+
name: kubernetesNameSchema.describe("Resource name"),
|
|
37
63
|
});
|
|
38
64
|
export const restartPodSchema = z.object({
|
|
39
|
-
namespace:
|
|
40
|
-
pod:
|
|
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:
|
|
44
|
-
deployment:
|
|
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:
|
|
48
|
-
deployment:
|
|
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:
|
|
78
|
+
namespace: namespaceNameSchema.describe("Kubernetes namespace"),
|
|
53
79
|
});
|
|
54
80
|
export const setResourcesSchema = z.object({
|
|
55
|
-
namespace:
|
|
56
|
-
deployment:
|
|
57
|
-
container:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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) {
|