@blogic-cz/agent-tools 0.2.3 → 0.2.5
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/package.json +4 -4
- package/src/k8s-tool/errors.ts +16 -1
- package/src/k8s-tool/index.ts +12 -0
- package/src/k8s-tool/security.ts +99 -0
- package/src/k8s-tool/service.ts +37 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/agent-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, and sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -64,13 +64,13 @@
|
|
|
64
64
|
"test": "vitest run"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@effect/platform-bun": "4.0.0-beta.
|
|
67
|
+
"@effect/platform-bun": "4.0.0-beta.25",
|
|
68
68
|
"@toon-format/toon": "2.1.0",
|
|
69
|
-
"effect": "4.0.0-beta.
|
|
69
|
+
"effect": "4.0.0-beta.25"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@effect/language-service": "0.77.0",
|
|
73
|
-
"@effect/vitest": "4.0.0-beta.
|
|
73
|
+
"@effect/vitest": "4.0.0-beta.25",
|
|
74
74
|
"@types/bun": "1.3.9",
|
|
75
75
|
"oxfmt": "0.35.0",
|
|
76
76
|
"oxlint": "1.50.0",
|
package/src/k8s-tool/errors.ts
CHANGED
|
@@ -27,4 +27,19 @@ export class K8sTimeoutError extends Schema.TaggedErrorClass<K8sTimeoutError>()(
|
|
|
27
27
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
28
28
|
}) {}
|
|
29
29
|
|
|
30
|
-
export
|
|
30
|
+
export class K8sDangerousCommandError extends Schema.TaggedErrorClass<K8sDangerousCommandError>()(
|
|
31
|
+
"K8sDangerousCommandError",
|
|
32
|
+
{
|
|
33
|
+
message: Schema.String,
|
|
34
|
+
command: Schema.String,
|
|
35
|
+
verb: Schema.optionalKey(Schema.String),
|
|
36
|
+
hint: Schema.optionalKey(Schema.String),
|
|
37
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
38
|
+
},
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
export type K8sError =
|
|
42
|
+
| K8sContextError
|
|
43
|
+
| K8sCommandError
|
|
44
|
+
| K8sTimeoutError
|
|
45
|
+
| K8sDangerousCommandError;
|
package/src/k8s-tool/index.ts
CHANGED
|
@@ -110,6 +110,18 @@ const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
|
110
110
|
};
|
|
111
111
|
return Effect.succeed(errorResult);
|
|
112
112
|
},
|
|
113
|
+
K8sDangerousCommandError: (error) => {
|
|
114
|
+
const errorResult: CommandResult = {
|
|
115
|
+
success: false,
|
|
116
|
+
error: error.message,
|
|
117
|
+
command: error.command,
|
|
118
|
+
hint:
|
|
119
|
+
error.hint ??
|
|
120
|
+
"AI agents can only run read-only kubectl commands. For mutating operations, use kubectl directly or ask a human operator.",
|
|
121
|
+
executionTimeMs: 0,
|
|
122
|
+
};
|
|
123
|
+
return Effect.succeed(errorResult);
|
|
124
|
+
},
|
|
113
125
|
}),
|
|
114
126
|
);
|
|
115
127
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K8s Security Module
|
|
3
|
+
*
|
|
4
|
+
* Validates kubectl commands before execution. Only read-only operations
|
|
5
|
+
* are allowed for AI agents. Mutating operations (delete, apply, patch, etc.)
|
|
6
|
+
* are blocked to prevent accidental or unauthorized changes to clusters.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Kubectl verbs that are safe for AI agents (read-only / non-destructive) */
|
|
10
|
+
export const ALLOWED_KUBECTL_VERBS = [
|
|
11
|
+
"get",
|
|
12
|
+
"describe",
|
|
13
|
+
"logs",
|
|
14
|
+
"top",
|
|
15
|
+
"explain",
|
|
16
|
+
"api-resources",
|
|
17
|
+
"api-versions",
|
|
18
|
+
"version",
|
|
19
|
+
"cluster-info",
|
|
20
|
+
"auth",
|
|
21
|
+
"diff",
|
|
22
|
+
"wait",
|
|
23
|
+
"exec",
|
|
24
|
+
"port-forward",
|
|
25
|
+
"config",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/** Kubectl verbs that are explicitly blocked (mutating / destructive) */
|
|
29
|
+
export const BLOCKED_KUBECTL_VERBS = [
|
|
30
|
+
"delete",
|
|
31
|
+
"drain",
|
|
32
|
+
"cordon",
|
|
33
|
+
"uncordon",
|
|
34
|
+
"taint",
|
|
35
|
+
"apply",
|
|
36
|
+
"patch",
|
|
37
|
+
"edit",
|
|
38
|
+
"replace",
|
|
39
|
+
"create",
|
|
40
|
+
"scale",
|
|
41
|
+
"rollout",
|
|
42
|
+
"set",
|
|
43
|
+
"label",
|
|
44
|
+
"annotate",
|
|
45
|
+
"expose",
|
|
46
|
+
"autoscale",
|
|
47
|
+
"run",
|
|
48
|
+
"cp",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export type K8sSecurityCheckResult = {
|
|
52
|
+
allowed: boolean;
|
|
53
|
+
command: string;
|
|
54
|
+
reason?: string;
|
|
55
|
+
verb?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks if a kubectl command is allowed for AI agent execution.
|
|
60
|
+
* Extracts the kubectl verb and validates against allow/block lists.
|
|
61
|
+
*
|
|
62
|
+
* Handles piped commands by checking only the kubectl portion (before first pipe).
|
|
63
|
+
*/
|
|
64
|
+
export function isKubectlCommandAllowed(cmd: string): K8sSecurityCheckResult {
|
|
65
|
+
const trimmed = cmd.trim();
|
|
66
|
+
|
|
67
|
+
// Handle piped commands — only validate the kubectl verb (before first |)
|
|
68
|
+
const kubectlPart = trimmed.split("|")[0].trim();
|
|
69
|
+
|
|
70
|
+
// Extract verb: first non-flag word
|
|
71
|
+
const words = kubectlPart.split(/\s+/).filter((w) => !w.startsWith("-"));
|
|
72
|
+
const verb = words[0]?.toLowerCase();
|
|
73
|
+
|
|
74
|
+
if (!verb) {
|
|
75
|
+
return { allowed: false, command: cmd, reason: "Empty kubectl command." };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Explicit blocklist — clear message about what's blocked and why
|
|
79
|
+
if ((BLOCKED_KUBECTL_VERBS as readonly string[]).includes(verb)) {
|
|
80
|
+
return {
|
|
81
|
+
allowed: false,
|
|
82
|
+
command: cmd,
|
|
83
|
+
verb,
|
|
84
|
+
reason: `'${verb}' is a mutating operation blocked for AI agents. Only read-only operations are allowed: ${ALLOWED_KUBECTL_VERBS.join(", ")}.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Allowlist — unknown verbs are also blocked for safety
|
|
89
|
+
if (!(ALLOWED_KUBECTL_VERBS as readonly string[]).includes(verb)) {
|
|
90
|
+
return {
|
|
91
|
+
allowed: false,
|
|
92
|
+
command: cmd,
|
|
93
|
+
verb,
|
|
94
|
+
reason: `Unknown kubectl verb '${verb}'. Only known read-only operations are allowed: ${ALLOWED_KUBECTL_VERBS.join(", ")}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { allowed: true, command: cmd, verb };
|
|
99
|
+
}
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -3,9 +3,15 @@ import { Effect, Layer, Option, Ref, ServiceMap, Stream } from "effect";
|
|
|
3
3
|
|
|
4
4
|
import type { CommandResult, Environment } from "./types";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
K8sCommandError,
|
|
8
|
+
K8sContextError,
|
|
9
|
+
K8sDangerousCommandError,
|
|
10
|
+
K8sTimeoutError,
|
|
11
|
+
} from "./errors";
|
|
7
12
|
import { ConfigService, getToolConfig } from "#config";
|
|
8
13
|
import type { K8sConfig } from "#config";
|
|
14
|
+
import { isKubectlCommandAllowed } from "./security";
|
|
9
15
|
|
|
10
16
|
export class K8sService extends ServiceMap.Service<
|
|
11
17
|
K8sService,
|
|
@@ -13,11 +19,17 @@ export class K8sService extends ServiceMap.Service<
|
|
|
13
19
|
readonly runCommand: (
|
|
14
20
|
cmd: string,
|
|
15
21
|
env: Environment,
|
|
16
|
-
) => Effect.Effect<
|
|
22
|
+
) => Effect.Effect<
|
|
23
|
+
string,
|
|
24
|
+
K8sContextError | K8sCommandError | K8sTimeoutError | K8sDangerousCommandError
|
|
25
|
+
>;
|
|
17
26
|
readonly runKubectl: (
|
|
18
27
|
cmd: string,
|
|
19
28
|
dryRun: boolean,
|
|
20
|
-
) => Effect.Effect<
|
|
29
|
+
) => Effect.Effect<
|
|
30
|
+
CommandResult,
|
|
31
|
+
K8sContextError | K8sCommandError | K8sTimeoutError | K8sDangerousCommandError
|
|
32
|
+
>;
|
|
21
33
|
}
|
|
22
34
|
>()("@agent-tools/K8sService") {
|
|
23
35
|
static readonly layer = Layer.effect(
|
|
@@ -166,8 +178,18 @@ export class K8sService extends ServiceMap.Service<
|
|
|
166
178
|
cmd: string,
|
|
167
179
|
_env: Environment,
|
|
168
180
|
) {
|
|
169
|
-
|
|
181
|
+
// Security: block dangerous commands before execution
|
|
182
|
+
const securityCheck = isKubectlCommandAllowed(cmd);
|
|
183
|
+
if (!securityCheck.allowed) {
|
|
184
|
+
return yield* new K8sDangerousCommandError({
|
|
185
|
+
message: securityCheck.reason ?? "Command not allowed",
|
|
186
|
+
command: cmd,
|
|
187
|
+
verb: securityCheck.verb,
|
|
188
|
+
hint: "AI agents can only run read-only kubectl commands. For mutating operations, use kubectl directly or ask a human operator.",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
170
191
|
|
|
192
|
+
const result = yield* executeCommand(cmd);
|
|
171
193
|
if (result.exitCode !== 0) {
|
|
172
194
|
return yield* new K8sCommandError({
|
|
173
195
|
message: result.stderr ?? `kubectl exited with code ${result.exitCode}`,
|
|
@@ -184,8 +206,18 @@ export class K8sService extends ServiceMap.Service<
|
|
|
184
206
|
cmd: string,
|
|
185
207
|
dryRun: boolean,
|
|
186
208
|
) {
|
|
187
|
-
|
|
209
|
+
// Security: block dangerous commands before execution (even dry-run)
|
|
210
|
+
const securityCheck = isKubectlCommandAllowed(cmd);
|
|
211
|
+
if (!securityCheck.allowed) {
|
|
212
|
+
return yield* new K8sDangerousCommandError({
|
|
213
|
+
message: securityCheck.reason ?? "Command not allowed",
|
|
214
|
+
command: cmd,
|
|
215
|
+
verb: securityCheck.verb,
|
|
216
|
+
hint: "AI agents can only run read-only kubectl commands. For mutating operations, use kubectl directly or ask a human operator.",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
188
219
|
|
|
220
|
+
const startTime = Date.now();
|
|
189
221
|
if (dryRun) {
|
|
190
222
|
const context = yield* resolveContext();
|
|
191
223
|
const fullCommand = `kubectl --context ${context} ${cmd}`;
|