triagent 0.1.0-alpha1 → 0.1.0-alpha12
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/README.md +102 -7
- package/bunfig.toml +4 -0
- package/package.json +12 -5
- package/src/cli/config.ts +23 -2
- package/src/config.ts +25 -3
- package/src/index.ts +23 -4
- package/src/mastra/agents/debugger.ts +76 -21
- package/src/mastra/index.ts +2 -2
- package/src/mastra/tools/cli.ts +65 -0
- package/src/sandbox/bashlet.ts +12 -5
- package/src/server/webhook.ts +4 -0
- package/src/tui/app.tsx +108 -6
- package/src/mastra/tools/kubectl.ts +0 -107
package/README.md
CHANGED
|
@@ -5,7 +5,17 @@ AI-powered Kubernetes debugging agent with terminal UI.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun
|
|
8
|
+
# bun
|
|
9
|
+
bun install -g triagent
|
|
10
|
+
|
|
11
|
+
# npm
|
|
12
|
+
npm install -g triagent
|
|
13
|
+
|
|
14
|
+
# yarn
|
|
15
|
+
yarn global add triagent
|
|
16
|
+
|
|
17
|
+
# pnpm
|
|
18
|
+
pnpm add -g triagent
|
|
9
19
|
```
|
|
10
20
|
|
|
11
21
|
## Usage
|
|
@@ -20,14 +30,99 @@ triagent --webhook-only
|
|
|
20
30
|
|
|
21
31
|
## Configuration
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
Configuration can be set via CLI commands or environment variables. CLI config takes precedence over environment variables.
|
|
34
|
+
|
|
35
|
+
### CLI Config
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Set configuration values
|
|
39
|
+
triagent config set <key> <value>
|
|
40
|
+
|
|
41
|
+
# Get a configuration value
|
|
42
|
+
triagent config get <key>
|
|
43
|
+
|
|
44
|
+
# List all configuration values
|
|
45
|
+
triagent config list
|
|
46
|
+
|
|
47
|
+
# Show config file path
|
|
48
|
+
triagent config path
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Config Keys
|
|
52
|
+
|
|
53
|
+
| Key | Description | Default |
|
|
54
|
+
|-----|-------------|---------|
|
|
55
|
+
| `aiProvider` | AI provider (`openai`, `anthropic`, `google`) | `anthropic` |
|
|
56
|
+
| `aiModel` | Model ID (e.g., `gpt-4o`, `claude-sonnet-4-20250514`) | Provider default |
|
|
57
|
+
| `apiKey` | API key for the provider | - |
|
|
58
|
+
| `baseUrl` | Custom API base URL (for proxies or local models) | - |
|
|
59
|
+
| `webhookPort` | Webhook server port | `3000` |
|
|
60
|
+
| `codebasePath` | Path to single codebase (legacy) | `./` |
|
|
61
|
+
| `kubeConfigPath` | Kubernetes config path | `~/.kube` |
|
|
62
|
+
|
|
63
|
+
### Multiple Codebases
|
|
64
|
+
|
|
65
|
+
For applications spanning multiple repositories, configure `codebasePaths` in `~/.config/triagent/config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"codebasePaths": [
|
|
70
|
+
{ "name": "frontend", "path": "/path/to/frontend-repo" },
|
|
71
|
+
{ "name": "backend", "path": "/path/to/backend-repo" },
|
|
72
|
+
{ "name": "infra", "path": "/path/to/infrastructure" }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each codebase is mounted at `/workspace/<name>` in the sandbox. The model can access any codebase as needed during investigation.
|
|
78
|
+
|
|
79
|
+
### Custom Instructions (TRIAGENT.md)
|
|
80
|
+
|
|
81
|
+
Create `~/.config/triagent/TRIAGENT.md` to provide custom instructions to the model. These instructions are prepended to the default system prompt.
|
|
82
|
+
|
|
83
|
+
Example `TRIAGENT.md`:
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
## Project Context
|
|
87
|
+
|
|
88
|
+
This is a microservices e-commerce platform with the following services:
|
|
89
|
+
- frontend: Next.js app in /workspace/frontend
|
|
90
|
+
- api: Go backend in /workspace/backend
|
|
91
|
+
- infra: Terraform configs in /workspace/infra
|
|
92
|
+
|
|
93
|
+
## Investigation Priorities
|
|
94
|
+
|
|
95
|
+
1. Always check the api service logs first for 5xx errors
|
|
96
|
+
2. The frontend service talks to api via internal DNS: api.default.svc.cluster.local
|
|
97
|
+
3. Common issues: Redis connection timeouts, PostgreSQL connection pool exhaustion
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Environment Variables
|
|
101
|
+
|
|
102
|
+
| Variable | Description |
|
|
103
|
+
|----------|-------------|
|
|
104
|
+
| `AI_PROVIDER` | AI provider (`openai`, `anthropic`, `google`) |
|
|
105
|
+
| `AI_MODEL` | Model ID |
|
|
106
|
+
| `AI_BASE_URL` | Custom API base URL |
|
|
107
|
+
| `OPENAI_API_KEY` | OpenAI API key |
|
|
108
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
109
|
+
| `GOOGLE_GENERATIVE_AI_API_KEY` | Google AI API key |
|
|
110
|
+
| `WEBHOOK_PORT` | Webhook server port |
|
|
111
|
+
| `CODEBASE_PATH` | Path to codebase |
|
|
112
|
+
| `KUBE_CONFIG_PATH` | Kubernetes config path |
|
|
113
|
+
|
|
114
|
+
### Examples
|
|
24
115
|
|
|
25
116
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
117
|
+
# Configure with Anthropic (default)
|
|
118
|
+
triagent config set apiKey sk-ant-...
|
|
119
|
+
|
|
120
|
+
# Configure with OpenAI
|
|
121
|
+
triagent config set aiProvider openai
|
|
122
|
+
triagent config set apiKey sk-proj-...
|
|
123
|
+
|
|
124
|
+
# Use a custom API endpoint (e.g., proxy or local model)
|
|
125
|
+
triagent config set baseUrl https://your-proxy.example.com/v1
|
|
31
126
|
```
|
|
32
127
|
|
|
33
128
|
## Development
|
package/bunfig.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triagent",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-alpha12",
|
|
4
4
|
"description": "AI-powered Kubernetes debugging agent with terminal UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/
|
|
30
|
+
"url": "git+https://github.com/ServiceWeave/triagent.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/ServiceWeave/triagent#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/ServiceWeave/triagent/issues"
|
|
31
35
|
},
|
|
32
36
|
"license": "MIT",
|
|
33
37
|
"scripts": {
|
|
@@ -46,12 +50,15 @@
|
|
|
46
50
|
"@opentui/core": "^0.1.72",
|
|
47
51
|
"@opentui/solid": "^0.1.72",
|
|
48
52
|
"hono": "^4.6.0",
|
|
49
|
-
"
|
|
53
|
+
"opentui-spinner": "^0.0.6",
|
|
54
|
+
"solid-js": "1.9.9",
|
|
55
|
+
"triagent": "^0.1.0-alpha1",
|
|
50
56
|
"zod": "^3.24.0"
|
|
51
57
|
},
|
|
52
58
|
"devDependencies": {
|
|
53
|
-
"
|
|
59
|
+
"@types/babel__core": "^7.20.5",
|
|
54
60
|
"@types/node": "^22.0.0",
|
|
55
|
-
"bun-types": "^1.2.0"
|
|
61
|
+
"bun-types": "^1.2.0",
|
|
62
|
+
"typescript": "^5.7.0"
|
|
56
63
|
}
|
|
57
64
|
}
|
package/src/cli/config.ts
CHANGED
|
@@ -3,22 +3,43 @@ import { homedir } from "os";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import type { AIProvider } from "../config.js";
|
|
5
5
|
|
|
6
|
+
export interface CodebaseEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface StoredConfig {
|
|
7
12
|
aiProvider?: AIProvider;
|
|
8
13
|
aiModel?: string;
|
|
9
14
|
apiKey?: string;
|
|
15
|
+
baseUrl?: string;
|
|
10
16
|
webhookPort?: number;
|
|
11
|
-
codebasePath?: string;
|
|
17
|
+
codebasePath?: string; // Deprecated: use codebasePaths instead
|
|
18
|
+
codebasePaths?: CodebaseEntry[];
|
|
12
19
|
kubeConfigPath?: string;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
const CONFIG_DIR = join(homedir(), ".config", "triagent");
|
|
16
23
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
24
|
+
const TRIAGENT_MD_FILE = join(CONFIG_DIR, "TRIAGENT.md");
|
|
17
25
|
|
|
18
26
|
export async function getConfigPath(): Promise<string> {
|
|
19
27
|
return CONFIG_FILE;
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
export async function getTriagentMdPath(): Promise<string> {
|
|
31
|
+
return TRIAGENT_MD_FILE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadTriagentMd(): Promise<string | null> {
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile(TRIAGENT_MD_FILE, "utf-8");
|
|
37
|
+
return content.trim();
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export async function loadStoredConfig(): Promise<StoredConfig> {
|
|
23
44
|
try {
|
|
24
45
|
const content = await readFile(CONFIG_FILE, "utf-8");
|
|
@@ -39,7 +60,7 @@ export async function setConfigValue(key: keyof StoredConfig, value: string | nu
|
|
|
39
60
|
await saveStoredConfig(config);
|
|
40
61
|
}
|
|
41
62
|
|
|
42
|
-
export async function getConfigValue(key: keyof StoredConfig): Promise<
|
|
63
|
+
export async function getConfigValue(key: keyof StoredConfig): Promise<StoredConfig[keyof StoredConfig]> {
|
|
43
64
|
const config = await loadStoredConfig();
|
|
44
65
|
return config[key];
|
|
45
66
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import { loadStoredConfig, type StoredConfig } from "./cli/config.js";
|
|
4
|
+
import { loadStoredConfig, type StoredConfig, type CodebaseEntry } from "./cli/config.js";
|
|
5
5
|
|
|
6
6
|
const AIProviderSchema = z.enum(["openai", "anthropic", "google"]);
|
|
7
7
|
export type AIProvider = z.infer<typeof AIProviderSchema>;
|
|
8
8
|
|
|
9
|
+
const CodebaseEntrySchema = z.object({
|
|
10
|
+
name: z.string().min(1),
|
|
11
|
+
path: z.string().min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
9
14
|
const ConfigSchema = z.object({
|
|
10
15
|
aiProvider: AIProviderSchema,
|
|
11
16
|
aiModel: z.string().min(1),
|
|
12
17
|
apiKey: z.string().min(1),
|
|
18
|
+
baseUrl: z.string().url().optional(),
|
|
13
19
|
webhookPort: z.number().int().positive().default(3000),
|
|
14
|
-
|
|
20
|
+
codebasePaths: z.array(CodebaseEntrySchema).min(1),
|
|
15
21
|
kubeConfigPath: z.string().min(1).default("~/.kube"),
|
|
16
22
|
});
|
|
17
23
|
|
|
18
24
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
25
|
+
export type { CodebaseEntry };
|
|
19
26
|
|
|
20
27
|
function expandPath(path: string): string {
|
|
21
28
|
if (path.startsWith("~")) {
|
|
@@ -38,6 +45,20 @@ function getApiKey(provider: AIProvider, stored: StoredConfig): string {
|
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function resolveCodebasePaths(stored: StoredConfig): CodebaseEntry[] {
|
|
49
|
+
// Priority: codebasePaths array > legacy codebasePath > default
|
|
50
|
+
if (stored.codebasePaths && stored.codebasePaths.length > 0) {
|
|
51
|
+
return stored.codebasePaths.map((entry) => ({
|
|
52
|
+
name: entry.name,
|
|
53
|
+
path: expandPath(entry.path),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Backward compatibility: convert single codebasePath to array
|
|
58
|
+
const legacyPath = process.env.CODEBASE_PATH || stored.codebasePath || "./";
|
|
59
|
+
return [{ name: "workspace", path: expandPath(legacyPath) }];
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
export async function loadConfig(): Promise<Config> {
|
|
42
63
|
const stored = await loadStoredConfig();
|
|
43
64
|
|
|
@@ -47,8 +68,9 @@ export async function loadConfig(): Promise<Config> {
|
|
|
47
68
|
aiProvider: provider,
|
|
48
69
|
aiModel: process.env.AI_MODEL || stored.aiModel || getDefaultModel(provider),
|
|
49
70
|
apiKey: getApiKey(provider, stored),
|
|
71
|
+
baseUrl: process.env.AI_BASE_URL || stored.baseUrl || undefined,
|
|
50
72
|
webhookPort: parseInt(process.env.WEBHOOK_PORT || String(stored.webhookPort || 3000), 10),
|
|
51
|
-
|
|
73
|
+
codebasePaths: resolveCodebasePaths(stored),
|
|
52
74
|
kubeConfigPath: expandPath(process.env.KUBE_CONFIG_PATH || stored.kubeConfigPath || "~/.kube"),
|
|
53
75
|
};
|
|
54
76
|
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// Load solid JSX plugin before any TSX imports
|
|
3
|
+
import "@opentui/solid/preload";
|
|
4
|
+
|
|
2
5
|
import { loadConfig } from "./config.js";
|
|
3
6
|
import { initSandboxFromConfig } from "./sandbox/bashlet.js";
|
|
4
7
|
import { createMastraInstance, buildIncidentPrompt, getDebuggerAgent } from "./mastra/index.js";
|
|
5
|
-
import { runTUI } from "./tui/app.jsx";
|
|
6
8
|
import { startWebhookServer } from "./server/webhook.js";
|
|
7
9
|
import {
|
|
8
10
|
loadStoredConfig,
|
|
@@ -84,10 +86,18 @@ CONFIG KEYS:
|
|
|
84
86
|
aiProvider - AI provider (openai, anthropic, google)
|
|
85
87
|
aiModel - Model ID (e.g., gpt-4o, claude-sonnet-4-20250514)
|
|
86
88
|
apiKey - API key for the provider
|
|
89
|
+
baseUrl - Custom API base URL (for proxies or local models)
|
|
87
90
|
webhookPort - Webhook server port (default: 3000)
|
|
88
|
-
codebasePath - Path to codebase (default: ./)
|
|
91
|
+
codebasePath - Path to codebase (default: ./) - for single codebase
|
|
89
92
|
kubeConfigPath - Kubernetes config path (default: ~/.kube)
|
|
90
93
|
|
|
94
|
+
For multiple codebases, edit ~/.config/triagent/config.json directly:
|
|
95
|
+
"codebasePaths": [
|
|
96
|
+
{ "name": "frontend", "path": "/path/to/frontend" },
|
|
97
|
+
{ "name": "backend", "path": "/path/to/backend" }
|
|
98
|
+
]
|
|
99
|
+
Each codebase will be mounted at /workspace/<name> in the sandbox.
|
|
100
|
+
|
|
91
101
|
MODES:
|
|
92
102
|
Interactive (default):
|
|
93
103
|
Run with no arguments to start the interactive TUI.
|
|
@@ -109,6 +119,7 @@ MODES:
|
|
|
109
119
|
ENVIRONMENT VARIABLES:
|
|
110
120
|
AI_PROVIDER - AI provider (openai, anthropic, google)
|
|
111
121
|
AI_MODEL - Model ID (e.g., gpt-4o, claude-3-5-sonnet)
|
|
122
|
+
AI_BASE_URL - Custom API base URL (for proxies or local models)
|
|
112
123
|
OPENAI_API_KEY - OpenAI API key
|
|
113
124
|
ANTHROPIC_API_KEY - Anthropic API key
|
|
114
125
|
GOOGLE_GENERATIVE_AI_API_KEY - Google AI API key
|
|
@@ -152,7 +163,12 @@ async function runDirectIncident(description: string): Promise<void> {
|
|
|
152
163
|
if (toolCalls && toolCalls.length > 0) {
|
|
153
164
|
const toolCall = toolCalls[0];
|
|
154
165
|
const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
|
|
155
|
-
|
|
166
|
+
const args = "args" in toolCall ? toolCall.args : {};
|
|
167
|
+
console.log(`\n[Tool: ${toolName}]`);
|
|
168
|
+
if (args && typeof args === "object" && "command" in args) {
|
|
169
|
+
console.log(`$ ${args.command}`);
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
156
172
|
}
|
|
157
173
|
},
|
|
158
174
|
});
|
|
@@ -174,6 +190,7 @@ async function handleConfigCommand(args: CliArgs): Promise<void> {
|
|
|
174
190
|
"aiProvider",
|
|
175
191
|
"aiModel",
|
|
176
192
|
"apiKey",
|
|
193
|
+
"baseUrl",
|
|
177
194
|
"webhookPort",
|
|
178
195
|
"codebasePath",
|
|
179
196
|
"kubeConfigPath",
|
|
@@ -270,7 +287,7 @@ async function main(): Promise<void> {
|
|
|
270
287
|
// Initialize sandbox and Mastra
|
|
271
288
|
try {
|
|
272
289
|
initSandboxFromConfig(config, args.host);
|
|
273
|
-
createMastraInstance(config);
|
|
290
|
+
await createMastraInstance(config);
|
|
274
291
|
if (args.host) {
|
|
275
292
|
console.log("⚠️ Running in host mode (no sandbox)\n");
|
|
276
293
|
}
|
|
@@ -289,6 +306,8 @@ async function main(): Promise<void> {
|
|
|
289
306
|
} else {
|
|
290
307
|
// Interactive TUI mode
|
|
291
308
|
console.log("Starting Triagent TUI...\n");
|
|
309
|
+
// Dynamic import to ensure solid plugin is loaded first
|
|
310
|
+
const { runTUI } = await import("./tui/app.jsx");
|
|
292
311
|
const tui = await runTUI();
|
|
293
312
|
|
|
294
313
|
// Handle graceful shutdown
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { Agent } from "@mastra/core/agent";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { cliTool } from "../tools/cli.js";
|
|
4
4
|
import { gitTool } from "../tools/git.js";
|
|
5
5
|
import { filesystemTool } from "../tools/filesystem.js";
|
|
6
|
+
import { loadTriagentMd } from "../../cli/config.js";
|
|
6
7
|
import type { Config } from "../../config.js";
|
|
7
8
|
|
|
8
9
|
const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent named Triagent. Your role is to investigate and diagnose issues in Kubernetes clusters by analyzing resources, logs, code, and git history.
|
|
9
10
|
|
|
10
11
|
## Your Capabilities
|
|
11
12
|
|
|
12
|
-
1. **
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
1. **CLI Access** (cli tool):
|
|
14
|
+
- Run any shell command including kubectl, grep, awk, jq, curl, etc.
|
|
15
|
+
- Pipe commands together for powerful filtering and processing
|
|
16
|
+
- Examples:
|
|
17
|
+
- \`kubectl get pods -A | grep inventory\`
|
|
18
|
+
- \`kubectl logs deploy/myapp --tail 100 | grep -i error\`
|
|
19
|
+
- \`kubectl get pods -o json | jq '.items[].metadata.name'\`
|
|
20
|
+
- \`kubectl describe pod mypod | grep -A10 Events\`
|
|
18
21
|
|
|
19
22
|
2. **Code Analysis** (filesystem tool):
|
|
20
23
|
- Read source code files
|
|
@@ -27,6 +30,39 @@ const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent name
|
|
|
27
30
|
- Show specific commit details
|
|
28
31
|
- Blame files to find who changed what
|
|
29
32
|
|
|
33
|
+
## Resource Discovery Strategy
|
|
34
|
+
|
|
35
|
+
When asked to find resources for a service (e.g., "inventory service"), DO NOT simply try one label like \`app=inventory\` and give up if not found. Instead, use a systematic discovery approach:
|
|
36
|
+
|
|
37
|
+
1. **Search by partial name match using grep**:
|
|
38
|
+
- \`kubectl get pods -A | grep -i inventory\`
|
|
39
|
+
- \`kubectl get deploy,svc -A | grep -i inventory\`
|
|
40
|
+
- This finds resources with "inventory" anywhere in the name (e.g., \`inventory-api\`, \`svc-inventory\`)
|
|
41
|
+
|
|
42
|
+
2. **If grep returns no results, list all resources to browse**:
|
|
43
|
+
- \`kubectl get pods,deploy,svc -A\` to see everything
|
|
44
|
+
- \`kubectl get pods -n <namespace>\` if namespace is known
|
|
45
|
+
|
|
46
|
+
3. **Try common label patterns**:
|
|
47
|
+
- \`kubectl get pods -A -l app=inventory\`
|
|
48
|
+
- \`kubectl get pods -A -l app.kubernetes.io/name=inventory\`
|
|
49
|
+
- \`kubectl get pods -A -l component=inventory\`
|
|
50
|
+
|
|
51
|
+
4. **Follow the resource chain**:
|
|
52
|
+
- Found a Service? \`kubectl describe svc <name> | grep Selector\` then find pods with that selector
|
|
53
|
+
- Found a Deployment? \`kubectl get pods -l app=<deployment-name>\`
|
|
54
|
+
- Use \`kubectl get endpoints <svc-name>\` to see which pods back a service
|
|
55
|
+
|
|
56
|
+
5. **Check events for context**:
|
|
57
|
+
- \`kubectl get events -A --sort-by='.lastTimestamp' | grep -i inventory\`
|
|
58
|
+
- \`kubectl get events -A --sort-by='.lastTimestamp' | head -20\` for recent cluster activity
|
|
59
|
+
|
|
60
|
+
6. **When you find a potential match**:
|
|
61
|
+
- \`kubectl describe <resource> <name>\` to confirm it's the right one
|
|
62
|
+
- Check related resources (pods for a deployment, endpoints for a service)
|
|
63
|
+
|
|
64
|
+
Always report what you searched for and what you found, even if it's not an exact match. The user can confirm if you found the right resource.
|
|
65
|
+
|
|
30
66
|
## Investigation Process
|
|
31
67
|
|
|
32
68
|
When given an incident, follow this systematic approach:
|
|
@@ -36,26 +72,32 @@ When given an incident, follow this systematic approach:
|
|
|
36
72
|
- What symptoms are being observed
|
|
37
73
|
- When the issue started (if known)
|
|
38
74
|
|
|
39
|
-
2. **
|
|
40
|
-
-
|
|
41
|
-
-
|
|
75
|
+
2. **Discover Relevant Resources**:
|
|
76
|
+
- Use the Resource Discovery Strategy above to find the affected resources
|
|
77
|
+
- Don't assume exact names or labels - search broadly first
|
|
78
|
+
- Follow the resource chain (Service → Deployment → Pods → Containers)
|
|
79
|
+
|
|
80
|
+
3. **Check Cluster State**:
|
|
81
|
+
- Get pod status for discovered resources
|
|
82
|
+
- Check for recent events related to those resources
|
|
42
83
|
- Look at resource usage
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
- Fetch logs from affected pods
|
|
85
|
+
4. **Analyze Logs**:
|
|
86
|
+
- Fetch logs from affected pods (use \`--tail 100\` to get recent logs)
|
|
46
87
|
- Look for errors, exceptions, or unusual patterns
|
|
88
|
+
- If multiple containers, check each one
|
|
47
89
|
|
|
48
|
-
|
|
90
|
+
5. **Investigate Recent Changes**:
|
|
49
91
|
- Check git log for recent commits
|
|
50
92
|
- Review diffs of suspicious changes
|
|
51
93
|
- Correlate timing with when issues started
|
|
52
94
|
|
|
53
|
-
|
|
95
|
+
6. **Examine Code**:
|
|
54
96
|
- Read relevant configuration files
|
|
55
97
|
- Check application code if needed
|
|
56
98
|
- Look for misconfigurations
|
|
57
99
|
|
|
58
|
-
|
|
100
|
+
7. **Synthesize Findings**:
|
|
59
101
|
- Identify the root cause
|
|
60
102
|
- List affected resources
|
|
61
103
|
- Provide actionable recommendations
|
|
@@ -121,17 +163,30 @@ export const InvestigationResultSchema = z.object({
|
|
|
121
163
|
|
|
122
164
|
export type InvestigationResult = z.infer<typeof InvestigationResultSchema>;
|
|
123
165
|
|
|
124
|
-
export function createDebuggerAgent(config: Config) {
|
|
125
|
-
//
|
|
126
|
-
const
|
|
166
|
+
export async function createDebuggerAgent(config: Config) {
|
|
167
|
+
// Load user instructions from ~/.config/triagent/TRIAGENT.md if present
|
|
168
|
+
const userInstructions = await loadTriagentMd();
|
|
169
|
+
|
|
170
|
+
// Combine user instructions with default instructions
|
|
171
|
+
const instructions = userInstructions
|
|
172
|
+
? `## User-Provided Instructions\n\n${userInstructions}\n\n---\n\n${DEBUGGER_INSTRUCTIONS}`
|
|
173
|
+
: DEBUGGER_INSTRUCTIONS;
|
|
174
|
+
|
|
175
|
+
// Construct model config with API key and optional base URL
|
|
176
|
+
const modelId = `${config.aiProvider}/${config.aiModel}` as const;
|
|
177
|
+
const modelConfig = {
|
|
178
|
+
id: modelId,
|
|
179
|
+
apiKey: config.apiKey,
|
|
180
|
+
...(config.baseUrl && { url: config.baseUrl }),
|
|
181
|
+
};
|
|
127
182
|
|
|
128
183
|
return new Agent({
|
|
129
184
|
id: "kubernetes-debugger",
|
|
130
185
|
name: "Kubernetes Debugger",
|
|
131
|
-
instructions
|
|
132
|
-
model:
|
|
186
|
+
instructions,
|
|
187
|
+
model: modelConfig as any, // Mastra handles model routing
|
|
133
188
|
tools: {
|
|
134
|
-
|
|
189
|
+
cli: cliTool,
|
|
135
190
|
git: gitTool,
|
|
136
191
|
filesystem: filesystemTool,
|
|
137
192
|
},
|
package/src/mastra/index.ts
CHANGED
|
@@ -4,12 +4,12 @@ import type { Config } from "../config.js";
|
|
|
4
4
|
|
|
5
5
|
let mastraInstance: Mastra | null = null;
|
|
6
6
|
|
|
7
|
-
export function createMastraInstance(config: Config): Mastra {
|
|
7
|
+
export async function createMastraInstance(config: Config): Promise<Mastra> {
|
|
8
8
|
if (mastraInstance) {
|
|
9
9
|
return mastraInstance;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const debuggerAgent = createDebuggerAgent(config);
|
|
12
|
+
const debuggerAgent = await createDebuggerAgent(config);
|
|
13
13
|
|
|
14
14
|
mastraInstance = new Mastra({
|
|
15
15
|
agents: {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { execCommand } from "../../sandbox/bashlet.js";
|
|
4
|
+
|
|
5
|
+
interface CliOutput {
|
|
6
|
+
success: boolean;
|
|
7
|
+
output: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function filterSensitiveData(output: string): string {
|
|
12
|
+
// Redact potential secrets, tokens, and passwords
|
|
13
|
+
return output
|
|
14
|
+
.replace(
|
|
15
|
+
/(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
|
|
16
|
+
"$1: [REDACTED]"
|
|
17
|
+
)
|
|
18
|
+
.replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
|
|
19
|
+
.replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const cliTool = createTool({
|
|
23
|
+
id: "cli",
|
|
24
|
+
description: `Execute shell commands in the sandbox environment.
|
|
25
|
+
Use this to run any CLI commands including kubectl, grep, awk, jq, curl, etc.
|
|
26
|
+
Supports pipes and command chaining.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
- List all pods: kubectl get pods -A
|
|
30
|
+
- Find pods by name: kubectl get pods -A | grep inventory
|
|
31
|
+
- Get logs with filtering: kubectl logs deployment/myapp -n prod --tail 100 | grep -i error
|
|
32
|
+
- Check resource usage: kubectl top pods -n default
|
|
33
|
+
- Describe and search: kubectl describe pod mypod | grep -A5 "Events"
|
|
34
|
+
- JSON processing: kubectl get pods -o json | jq '.items[].metadata.name'`,
|
|
35
|
+
|
|
36
|
+
inputSchema: z.object({
|
|
37
|
+
command: z.string().describe("The shell command to execute"),
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
execute: async ({ command }): Promise<CliOutput> => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await execCommand(command);
|
|
43
|
+
|
|
44
|
+
if (result.exitCode !== 0) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
output: result.stdout ? filterSensitiveData(result.stdout) : "",
|
|
48
|
+
error: result.stderr || `Command failed with exit code ${result.exitCode}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
output: filterSensitiveData(result.stdout),
|
|
55
|
+
error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
output: "",
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
package/src/sandbox/bashlet.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Bashlet } from "@bashlet/sdk";
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { readFile as fsReadFile, readdir } from "fs/promises";
|
|
4
|
-
import type { Config } from "../config.js";
|
|
4
|
+
import type { Config, CodebaseEntry } from "../config.js";
|
|
5
5
|
|
|
6
6
|
export interface CommandResult {
|
|
7
7
|
stdout: string;
|
|
@@ -10,7 +10,7 @@ export interface CommandResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface SandboxOptions {
|
|
13
|
-
|
|
13
|
+
codebasePaths: CodebaseEntry[];
|
|
14
14
|
kubeConfigPath: string;
|
|
15
15
|
timeout?: number;
|
|
16
16
|
useHost?: boolean;
|
|
@@ -22,7 +22,8 @@ let hostWorkdir = "./";
|
|
|
22
22
|
|
|
23
23
|
export function createSandbox(options: SandboxOptions): void {
|
|
24
24
|
hostMode = options.useHost ?? false;
|
|
25
|
-
|
|
25
|
+
// Use first codebase as default working directory
|
|
26
|
+
hostWorkdir = options.codebasePaths[0]?.path || "./";
|
|
26
27
|
|
|
27
28
|
if (hostMode) {
|
|
28
29
|
return;
|
|
@@ -32,9 +33,15 @@ export function createSandbox(options: SandboxOptions): void {
|
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Mount each codebase at /workspace/<name>
|
|
37
|
+
const codebaseMounts = options.codebasePaths.map((entry) => ({
|
|
38
|
+
hostPath: entry.path,
|
|
39
|
+
guestPath: `/workspace/${entry.name}`,
|
|
40
|
+
}));
|
|
41
|
+
|
|
35
42
|
bashletInstance = new Bashlet({
|
|
36
43
|
mounts: [
|
|
37
|
-
|
|
44
|
+
...codebaseMounts,
|
|
38
45
|
{ hostPath: options.kubeConfigPath, guestPath: "/root/.kube" },
|
|
39
46
|
],
|
|
40
47
|
workdir: "/workspace",
|
|
@@ -143,7 +150,7 @@ export async function listDir(path: string): Promise<string[]> {
|
|
|
143
150
|
|
|
144
151
|
export function initSandboxFromConfig(config: Config, useHost: boolean = false): void {
|
|
145
152
|
createSandbox({
|
|
146
|
-
|
|
153
|
+
codebasePaths: config.codebasePaths,
|
|
147
154
|
kubeConfigPath: config.kubeConfigPath,
|
|
148
155
|
timeout: 120,
|
|
149
156
|
useHost,
|
package/src/server/webhook.ts
CHANGED
|
@@ -149,7 +149,11 @@ async function runInvestigation(id: string): Promise<void> {
|
|
|
149
149
|
if (toolCalls && toolCalls.length > 0) {
|
|
150
150
|
const toolCall = toolCalls[0];
|
|
151
151
|
const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
|
|
152
|
+
const args = "args" in toolCall ? toolCall.args : {};
|
|
152
153
|
console.log(`[Investigation ${id}] Tool: ${toolName}`);
|
|
154
|
+
if (args && typeof args === "object" && "command" in args) {
|
|
155
|
+
console.log(`[Investigation ${id}] $ ${args.command}`);
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
},
|
|
155
159
|
});
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
1
2
|
import { render } from "@opentui/solid";
|
|
2
3
|
import { createSignal, For, Show, onMount } from "solid-js";
|
|
3
4
|
import { createTextAttributes } from "@opentui/core";
|
|
5
|
+
import "opentui-spinner/solid";
|
|
4
6
|
import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
|
|
5
7
|
import type { IncidentInput } from "../mastra/agents/debugger.js";
|
|
6
8
|
|
|
@@ -10,6 +12,25 @@ interface Message {
|
|
|
10
12
|
content: string;
|
|
11
13
|
timestamp: Date;
|
|
12
14
|
toolName?: string;
|
|
15
|
+
command?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Conversation history for multi-turn debugging
|
|
19
|
+
interface ConversationMessage {
|
|
20
|
+
role: "user" | "assistant";
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: string): string {
|
|
25
|
+
if (history.length === 0) {
|
|
26
|
+
return newMessage;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const historyText = history
|
|
30
|
+
.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
|
|
31
|
+
.join("\n\n");
|
|
32
|
+
|
|
33
|
+
return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
|
|
13
34
|
}
|
|
14
35
|
|
|
15
36
|
type AppStatus = "idle" | "investigating" | "complete" | "error";
|
|
@@ -17,8 +38,55 @@ type AppStatus = "idle" | "investigating" | "complete" | "error";
|
|
|
17
38
|
const ATTR_DIM = createTextAttributes({ dim: true });
|
|
18
39
|
const ATTR_BOLD = createTextAttributes({ bold: true });
|
|
19
40
|
|
|
41
|
+
function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
|
|
42
|
+
if (!args || typeof args !== "object") return undefined;
|
|
43
|
+
|
|
44
|
+
const a = args as Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
switch (toolName) {
|
|
47
|
+
case "cli":
|
|
48
|
+
// CLI tool has direct command
|
|
49
|
+
return "command" in a ? String(a.command) : undefined;
|
|
50
|
+
|
|
51
|
+
case "git": {
|
|
52
|
+
// Build git command: git <command> [args...] [path]
|
|
53
|
+
if (!("command" in a)) return undefined;
|
|
54
|
+
const parts = ["git", String(a.command)];
|
|
55
|
+
if ("args" in a && Array.isArray(a.args)) {
|
|
56
|
+
parts.push(...a.args.map(String));
|
|
57
|
+
}
|
|
58
|
+
if ("path" in a && a.path) {
|
|
59
|
+
parts.push(String(a.path));
|
|
60
|
+
}
|
|
61
|
+
return parts.join(" ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "filesystem": {
|
|
65
|
+
// Build filesystem display: <operation> <path> [pattern]
|
|
66
|
+
if (!("operation" in a)) return undefined;
|
|
67
|
+
const op = String(a.operation);
|
|
68
|
+
const path = "path" in a ? String(a.path) : "";
|
|
69
|
+
if (op === "search" && "pattern" in a) {
|
|
70
|
+
return `grep "${a.pattern}" ${path}`;
|
|
71
|
+
}
|
|
72
|
+
if (op === "read") {
|
|
73
|
+
return `cat ${path}`;
|
|
74
|
+
}
|
|
75
|
+
if (op === "list") {
|
|
76
|
+
return `ls ${path}`;
|
|
77
|
+
}
|
|
78
|
+
return `${op} ${path}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
// Fallback: try to use command if it exists
|
|
83
|
+
return "command" in a ? String(a.command) : undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
20
87
|
function App() {
|
|
21
88
|
const [messages, setMessages] = createSignal<Message[]>([]);
|
|
89
|
+
const [conversationHistory, setConversationHistory] = createSignal<ConversationMessage[]>([]);
|
|
22
90
|
const [status, setStatus] = createSignal<AppStatus>("idle");
|
|
23
91
|
const [currentTool, setCurrentTool] = createSignal<string | null>(null);
|
|
24
92
|
const [inputValue, setInputValue] = createSignal("");
|
|
@@ -40,29 +108,50 @@ function App() {
|
|
|
40
108
|
setError(null);
|
|
41
109
|
setCurrentTool(null);
|
|
42
110
|
|
|
111
|
+
// Add user message to UI
|
|
43
112
|
addMessage({
|
|
44
113
|
role: "user",
|
|
45
114
|
content: incident.description,
|
|
46
115
|
});
|
|
47
116
|
|
|
117
|
+
// Build prompt: use full incident prompt for first message, include history for follow-ups
|
|
118
|
+
const isFirstMessage = conversationHistory().length === 0;
|
|
119
|
+
const userContent = isFirstMessage
|
|
120
|
+
? buildIncidentPrompt(incident)
|
|
121
|
+
: incident.description;
|
|
122
|
+
|
|
123
|
+
// Format prompt with conversation history
|
|
124
|
+
const prompt = formatHistoryAsPrompt(conversationHistory(), userContent);
|
|
125
|
+
|
|
126
|
+
// Add user message to conversation history
|
|
127
|
+
setConversationHistory((prev) => [
|
|
128
|
+
...prev,
|
|
129
|
+
{ role: "user", content: userContent },
|
|
130
|
+
]);
|
|
131
|
+
|
|
48
132
|
try {
|
|
49
133
|
const agent = getDebuggerAgent();
|
|
50
|
-
const prompt = buildIncidentPrompt(incident);
|
|
51
134
|
|
|
52
135
|
let assistantContent = "";
|
|
53
136
|
|
|
137
|
+
// Send the formatted prompt to the agent
|
|
54
138
|
const stream = await agent.stream(prompt, {
|
|
55
139
|
maxSteps: 20,
|
|
56
140
|
onStepFinish: ({ toolCalls }) => {
|
|
57
141
|
if (toolCalls && toolCalls.length > 0) {
|
|
58
|
-
const toolCall = toolCalls[0];
|
|
59
|
-
const toolName =
|
|
60
|
-
|
|
142
|
+
const toolCall = toolCalls[0] as { toolName?: string; args?: unknown };
|
|
143
|
+
const toolName = toolCall.toolName ?? "tool";
|
|
144
|
+
const args = toolCall.args ?? {};
|
|
145
|
+
|
|
146
|
+
// Build display command based on tool type
|
|
147
|
+
const command = buildDisplayCommand(toolName, args);
|
|
148
|
+
|
|
61
149
|
setCurrentTool(toolName);
|
|
62
150
|
addMessage({
|
|
63
151
|
role: "tool",
|
|
64
|
-
content: `Executing ${toolName}...`,
|
|
152
|
+
content: command ? `$ ${command}` : `Executing ${toolName}...`,
|
|
65
153
|
toolName,
|
|
154
|
+
command,
|
|
66
155
|
});
|
|
67
156
|
}
|
|
68
157
|
},
|
|
@@ -72,11 +161,18 @@ function App() {
|
|
|
72
161
|
assistantContent += chunk;
|
|
73
162
|
}
|
|
74
163
|
|
|
164
|
+
// Add assistant response to UI
|
|
75
165
|
addMessage({
|
|
76
166
|
role: "assistant",
|
|
77
167
|
content: assistantContent,
|
|
78
168
|
});
|
|
79
169
|
|
|
170
|
+
// Add assistant response to conversation history
|
|
171
|
+
setConversationHistory((prev) => [
|
|
172
|
+
...prev,
|
|
173
|
+
{ role: "assistant", content: assistantContent },
|
|
174
|
+
]);
|
|
175
|
+
|
|
80
176
|
setStatus("complete");
|
|
81
177
|
setCurrentTool(null);
|
|
82
178
|
} catch (err) {
|
|
@@ -185,7 +281,13 @@ function App() {
|
|
|
185
281
|
</box>
|
|
186
282
|
</Show>
|
|
187
283
|
<Show when={msg.role === "tool"}>
|
|
188
|
-
<box flexDirection="row" gap={1}>
|
|
284
|
+
<box flexDirection="row" gap={1} alignItems="center">
|
|
285
|
+
<Show
|
|
286
|
+
when={status() === "investigating" && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
|
|
287
|
+
fallback={<text fg="green">✓</text>}
|
|
288
|
+
>
|
|
289
|
+
<spinner name="dots" color="blue" />
|
|
290
|
+
</Show>
|
|
189
291
|
<text fg="blue" attributes={ATTR_DIM}>
|
|
190
292
|
[{msg.toolName}]
|
|
191
293
|
</text>
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { createTool } from "@mastra/core/tools";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { execCommand } from "../../sandbox/bashlet.js";
|
|
4
|
-
|
|
5
|
-
const ALLOWED_COMMANDS = ["get", "describe", "logs", "top", "events"] as const;
|
|
6
|
-
|
|
7
|
-
const KubectlInputSchema = z.object({
|
|
8
|
-
command: z.enum(ALLOWED_COMMANDS).describe("The kubectl command to run"),
|
|
9
|
-
resource: z
|
|
10
|
-
.string()
|
|
11
|
-
.optional()
|
|
12
|
-
.describe(
|
|
13
|
-
"Resource type (e.g., pods, deployments, services, configmaps, secrets)"
|
|
14
|
-
),
|
|
15
|
-
name: z.string().optional().describe("Specific resource name"),
|
|
16
|
-
namespace: z
|
|
17
|
-
.string()
|
|
18
|
-
.optional()
|
|
19
|
-
.describe("Kubernetes namespace (defaults to current context namespace)"),
|
|
20
|
-
flags: z
|
|
21
|
-
.array(z.string())
|
|
22
|
-
.optional()
|
|
23
|
-
.describe(
|
|
24
|
-
"Additional flags (e.g., ['-o', 'yaml'], ['--tail', '100'], ['-l', 'app=myapp'])"
|
|
25
|
-
),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Output type (no schema validation to allow error returns)
|
|
29
|
-
interface KubectlOutput {
|
|
30
|
-
success: boolean;
|
|
31
|
-
output: string;
|
|
32
|
-
error?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function filterSensitiveData(output: string): string {
|
|
36
|
-
// Redact potential secrets, tokens, and passwords
|
|
37
|
-
return output
|
|
38
|
-
.replace(
|
|
39
|
-
/(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
|
|
40
|
-
"$1: [REDACTED]"
|
|
41
|
-
)
|
|
42
|
-
.replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
|
|
43
|
-
.replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const kubectlTool = createTool({
|
|
47
|
-
id: "kubectl",
|
|
48
|
-
description: `Execute kubectl commands to inspect Kubernetes resources.
|
|
49
|
-
Available commands: ${ALLOWED_COMMANDS.join(", ")}.
|
|
50
|
-
Use this to get information about pods, deployments, services, logs, and cluster events.
|
|
51
|
-
Examples:
|
|
52
|
-
- Get all pods: { command: "get", resource: "pods", flags: ["-A"] }
|
|
53
|
-
- Get pod logs: { command: "logs", name: "my-pod", namespace: "default", flags: ["--tail", "100"] }
|
|
54
|
-
- Describe deployment: { command: "describe", resource: "deployment", name: "my-app" }
|
|
55
|
-
- Get events: { command: "events", namespace: "production", flags: ["--sort-by", ".lastTimestamp"] }`,
|
|
56
|
-
|
|
57
|
-
inputSchema: KubectlInputSchema,
|
|
58
|
-
|
|
59
|
-
execute: async (inputData): Promise<KubectlOutput> => {
|
|
60
|
-
const { command, resource, name, namespace, flags } = inputData;
|
|
61
|
-
|
|
62
|
-
// Build kubectl command
|
|
63
|
-
const parts = ["kubectl", command];
|
|
64
|
-
|
|
65
|
-
if (resource) {
|
|
66
|
-
parts.push(resource);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (name) {
|
|
70
|
-
parts.push(name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (namespace) {
|
|
74
|
-
parts.push("-n", namespace);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (flags && flags.length > 0) {
|
|
78
|
-
parts.push(...flags);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const fullCommand = parts.join(" ");
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await execCommand(fullCommand);
|
|
85
|
-
|
|
86
|
-
if (result.exitCode !== 0) {
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
output: "",
|
|
90
|
-
error: result.stderr || `Command failed with exit code ${result.exitCode}`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
success: true,
|
|
96
|
-
output: filterSensitiveData(result.stdout),
|
|
97
|
-
error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
|
|
98
|
-
};
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
output: "",
|
|
103
|
-
error: error instanceof Error ? error.message : String(error),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
});
|