triagent 0.1.0-alpha1
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 +51 -0
- package/bunfig.toml +1 -0
- package/package.json +57 -0
- package/src/cli/config.ts +60 -0
- package/src/config.ts +97 -0
- package/src/index.ts +310 -0
- package/src/mastra/agents/debugger.ts +176 -0
- package/src/mastra/index.ts +36 -0
- package/src/mastra/tools/filesystem.ts +129 -0
- package/src/mastra/tools/git.ts +83 -0
- package/src/mastra/tools/kubectl.ts +107 -0
- package/src/sandbox/bashlet.ts +151 -0
- package/src/server/webhook.ts +186 -0
- package/src/tui/app.tsx +281 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Agent } from "@mastra/core/agent";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { kubectlTool } from "../tools/kubectl.js";
|
|
4
|
+
import { gitTool } from "../tools/git.js";
|
|
5
|
+
import { filesystemTool } from "../tools/filesystem.js";
|
|
6
|
+
import type { Config } from "../../config.js";
|
|
7
|
+
|
|
8
|
+
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
|
+
## Your Capabilities
|
|
11
|
+
|
|
12
|
+
1. **Kubernetes Inspection** (kubectl tool):
|
|
13
|
+
- Get resource status (pods, deployments, services, configmaps)
|
|
14
|
+
- Describe resources for detailed information
|
|
15
|
+
- Fetch container logs
|
|
16
|
+
- Check resource usage (top)
|
|
17
|
+
- Review cluster events
|
|
18
|
+
|
|
19
|
+
2. **Code Analysis** (filesystem tool):
|
|
20
|
+
- Read source code files
|
|
21
|
+
- List directory structures
|
|
22
|
+
- Search for patterns in code
|
|
23
|
+
|
|
24
|
+
3. **Git History** (git tool):
|
|
25
|
+
- View recent commits
|
|
26
|
+
- Compare changes between commits
|
|
27
|
+
- Show specific commit details
|
|
28
|
+
- Blame files to find who changed what
|
|
29
|
+
|
|
30
|
+
## Investigation Process
|
|
31
|
+
|
|
32
|
+
When given an incident, follow this systematic approach:
|
|
33
|
+
|
|
34
|
+
1. **Understand the Issue**: Parse the incident description to identify:
|
|
35
|
+
- What service/component is affected
|
|
36
|
+
- What symptoms are being observed
|
|
37
|
+
- When the issue started (if known)
|
|
38
|
+
|
|
39
|
+
2. **Check Cluster State**:
|
|
40
|
+
- Get pod status for affected services
|
|
41
|
+
- Check for recent events
|
|
42
|
+
- Look at resource usage
|
|
43
|
+
|
|
44
|
+
3. **Analyze Logs**:
|
|
45
|
+
- Fetch logs from affected pods
|
|
46
|
+
- Look for errors, exceptions, or unusual patterns
|
|
47
|
+
|
|
48
|
+
4. **Investigate Recent Changes**:
|
|
49
|
+
- Check git log for recent commits
|
|
50
|
+
- Review diffs of suspicious changes
|
|
51
|
+
- Correlate timing with when issues started
|
|
52
|
+
|
|
53
|
+
5. **Examine Code**:
|
|
54
|
+
- Read relevant configuration files
|
|
55
|
+
- Check application code if needed
|
|
56
|
+
- Look for misconfigurations
|
|
57
|
+
|
|
58
|
+
6. **Synthesize Findings**:
|
|
59
|
+
- Identify the root cause
|
|
60
|
+
- List affected resources
|
|
61
|
+
- Provide actionable recommendations
|
|
62
|
+
|
|
63
|
+
## Output Format
|
|
64
|
+
|
|
65
|
+
Always provide your findings in a clear, structured format:
|
|
66
|
+
- **Summary**: Brief overview of the issue
|
|
67
|
+
- **Root Cause**: The identified cause of the problem
|
|
68
|
+
- **Evidence**: Specific data that supports your conclusion
|
|
69
|
+
- **Affected Resources**: List of impacted K8s resources
|
|
70
|
+
- **Recent Changes**: Relevant commits that might be related
|
|
71
|
+
- **Recommendations**: Specific steps to remediate the issue
|
|
72
|
+
|
|
73
|
+
## Important Guidelines
|
|
74
|
+
|
|
75
|
+
- Be thorough but efficient - don't run unnecessary commands
|
|
76
|
+
- Focus on actionable insights
|
|
77
|
+
- If unsure, state your confidence level
|
|
78
|
+
- Prioritize quick wins that can restore service
|
|
79
|
+
- Consider both application and infrastructure issues`;
|
|
80
|
+
|
|
81
|
+
export const InvestigationResultSchema = z.object({
|
|
82
|
+
summary: z.string().describe("Brief overview of the investigation"),
|
|
83
|
+
rootCause: z.string().describe("Identified root cause of the issue"),
|
|
84
|
+
confidence: z
|
|
85
|
+
.enum(["high", "medium", "low"])
|
|
86
|
+
.describe("Confidence level in the diagnosis"),
|
|
87
|
+
evidence: z
|
|
88
|
+
.array(z.string())
|
|
89
|
+
.describe("Specific evidence supporting the diagnosis"),
|
|
90
|
+
affectedResources: z
|
|
91
|
+
.array(
|
|
92
|
+
z.object({
|
|
93
|
+
type: z.string(),
|
|
94
|
+
name: z.string(),
|
|
95
|
+
namespace: z.string().optional(),
|
|
96
|
+
status: z.string(),
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
.describe("List of affected Kubernetes resources"),
|
|
100
|
+
recentChanges: z
|
|
101
|
+
.array(
|
|
102
|
+
z.object({
|
|
103
|
+
commit: z.string(),
|
|
104
|
+
message: z.string(),
|
|
105
|
+
author: z.string(),
|
|
106
|
+
relevance: z.string(),
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Relevant recent git commits"),
|
|
111
|
+
recommendations: z
|
|
112
|
+
.array(
|
|
113
|
+
z.object({
|
|
114
|
+
priority: z.enum(["critical", "high", "medium", "low"]),
|
|
115
|
+
action: z.string(),
|
|
116
|
+
details: z.string().optional(),
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
.describe("Recommended actions to resolve the issue"),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export type InvestigationResult = z.infer<typeof InvestigationResultSchema>;
|
|
123
|
+
|
|
124
|
+
export function createDebuggerAgent(config: Config) {
|
|
125
|
+
// Construct model string based on provider
|
|
126
|
+
const modelString = `${config.aiProvider}/${config.aiModel}`;
|
|
127
|
+
|
|
128
|
+
return new Agent({
|
|
129
|
+
id: "kubernetes-debugger",
|
|
130
|
+
name: "Kubernetes Debugger",
|
|
131
|
+
instructions: DEBUGGER_INSTRUCTIONS,
|
|
132
|
+
model: modelString as any, // Mastra handles model routing
|
|
133
|
+
tools: {
|
|
134
|
+
kubectl: kubectlTool,
|
|
135
|
+
git: gitTool,
|
|
136
|
+
filesystem: filesystemTool,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface IncidentInput {
|
|
142
|
+
title: string;
|
|
143
|
+
description: string;
|
|
144
|
+
severity?: "critical" | "warning" | "info";
|
|
145
|
+
labels?: Record<string, string>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildIncidentPrompt(incident: IncidentInput): string {
|
|
149
|
+
const parts = [
|
|
150
|
+
`# Incident Report`,
|
|
151
|
+
``,
|
|
152
|
+
`**Title**: ${incident.title}`,
|
|
153
|
+
``,
|
|
154
|
+
`**Description**: ${incident.description}`,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
if (incident.severity) {
|
|
158
|
+
parts.push(``, `**Severity**: ${incident.severity}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (incident.labels && Object.keys(incident.labels).length > 0) {
|
|
162
|
+
parts.push(``, `**Labels**:`);
|
|
163
|
+
for (const [key, value] of Object.entries(incident.labels)) {
|
|
164
|
+
parts.push(`- ${key}: ${value}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
parts.push(
|
|
169
|
+
``,
|
|
170
|
+
`---`,
|
|
171
|
+
``,
|
|
172
|
+
`Please investigate this incident and provide your findings.`
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return parts.join("\n");
|
|
176
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Mastra } from "@mastra/core";
|
|
2
|
+
import { createDebuggerAgent, buildIncidentPrompt, InvestigationResultSchema } from "./agents/debugger.js";
|
|
3
|
+
import type { Config } from "../config.js";
|
|
4
|
+
|
|
5
|
+
let mastraInstance: Mastra | null = null;
|
|
6
|
+
|
|
7
|
+
export function createMastraInstance(config: Config): Mastra {
|
|
8
|
+
if (mastraInstance) {
|
|
9
|
+
return mastraInstance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const debuggerAgent = createDebuggerAgent(config);
|
|
13
|
+
|
|
14
|
+
mastraInstance = new Mastra({
|
|
15
|
+
agents: {
|
|
16
|
+
debugger: debuggerAgent,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return mastraInstance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getMastra(): Mastra {
|
|
24
|
+
if (!mastraInstance) {
|
|
25
|
+
throw new Error("Mastra not initialized. Call createMastraInstance first.");
|
|
26
|
+
}
|
|
27
|
+
return mastraInstance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getDebuggerAgent() {
|
|
31
|
+
const mastra = getMastra();
|
|
32
|
+
return mastra.getAgent("debugger");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { createDebuggerAgent, buildIncidentPrompt, InvestigationResultSchema };
|
|
36
|
+
export type { IncidentInput, InvestigationResult } from "./agents/debugger.js";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFile, listDir, execCommand } from "../../sandbox/bashlet.js";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_OPERATIONS = ["read", "list", "search"] as const;
|
|
6
|
+
|
|
7
|
+
const FilesystemInputSchema = z.object({
|
|
8
|
+
operation: z.enum(ALLOWED_OPERATIONS).describe("The file operation to perform"),
|
|
9
|
+
path: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe(
|
|
12
|
+
"File or directory path relative to /workspace (the mounted codebase)"
|
|
13
|
+
),
|
|
14
|
+
pattern: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Search pattern for 'search' operation (grep-compatible regex)"),
|
|
18
|
+
maxLines: z
|
|
19
|
+
.number()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Maximum lines to return for read operation (default: 500)"),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Output type (no schema validation to allow error returns)
|
|
25
|
+
interface FilesystemOutput {
|
|
26
|
+
success: boolean;
|
|
27
|
+
content?: string;
|
|
28
|
+
entries?: string[];
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizePath(path: string): string {
|
|
33
|
+
// Ensure path stays within /workspace
|
|
34
|
+
const cleanPath = path
|
|
35
|
+
.replace(/\.\./g, "") // Remove parent directory references
|
|
36
|
+
.replace(/^\/+/, "") // Remove leading slashes
|
|
37
|
+
.replace(/\/+/g, "/"); // Normalize multiple slashes
|
|
38
|
+
|
|
39
|
+
return `/workspace/${cleanPath}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const filesystemTool = createTool({
|
|
43
|
+
id: "filesystem",
|
|
44
|
+
description: `Read files and list directories in the codebase.
|
|
45
|
+
Available operations:
|
|
46
|
+
- read: Read file contents (max 500 lines by default)
|
|
47
|
+
- list: List directory contents
|
|
48
|
+
- search: Search for pattern in files using grep
|
|
49
|
+
|
|
50
|
+
All paths are relative to the codebase root (/workspace).
|
|
51
|
+
Examples:
|
|
52
|
+
- Read a file: { operation: "read", path: "src/index.ts" }
|
|
53
|
+
- List directory: { operation: "list", path: "src/api" }
|
|
54
|
+
- Search for pattern: { operation: "search", path: "src", pattern: "async function" }
|
|
55
|
+
- Read with line limit: { operation: "read", path: "package.json", maxLines: 100 }`,
|
|
56
|
+
|
|
57
|
+
inputSchema: FilesystemInputSchema,
|
|
58
|
+
|
|
59
|
+
execute: async (inputData): Promise<FilesystemOutput> => {
|
|
60
|
+
const { operation, path, pattern, maxLines = 500 } = inputData;
|
|
61
|
+
const safePath = sanitizePath(path);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
switch (operation) {
|
|
65
|
+
case "read": {
|
|
66
|
+
const content = await readFile(safePath);
|
|
67
|
+
const lines = content.split("\n");
|
|
68
|
+
const truncated =
|
|
69
|
+
lines.length > maxLines
|
|
70
|
+
? lines.slice(0, maxLines).join("\n") +
|
|
71
|
+
`\n\n... [Truncated: showing ${maxLines} of ${lines.length} lines]`
|
|
72
|
+
: content;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
content: truncated,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case "list": {
|
|
81
|
+
const entries = await listDir(safePath);
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
entries,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "search": {
|
|
89
|
+
if (!pattern) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: "Pattern is required for search operation",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Use grep for searching
|
|
97
|
+
const grepCommand = `grep -rn "${pattern.replace(/"/g, '\\"')}" "${safePath}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --include="*.yaml" --include="*.yml" --include="*.md" | head -100`;
|
|
98
|
+
|
|
99
|
+
const result = await execCommand(grepCommand);
|
|
100
|
+
|
|
101
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
102
|
+
// grep returns 1 when no matches found
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: result.stderr || "Search failed",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
content:
|
|
112
|
+
result.stdout || "No matches found",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `Unknown operation: ${operation}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { execCommand } from "../../sandbox/bashlet.js";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_COMMANDS = ["log", "diff", "show", "blame"] as const;
|
|
6
|
+
|
|
7
|
+
const GitInputSchema = z.object({
|
|
8
|
+
command: z.enum(ALLOWED_COMMANDS).describe("The git command to run (read-only)"),
|
|
9
|
+
args: z
|
|
10
|
+
.array(z.string())
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
"Command arguments (e.g., ['--oneline', '-n', '20'] for log, ['HEAD~5..HEAD'] for diff)"
|
|
14
|
+
),
|
|
15
|
+
path: z.string().optional().describe("File or directory path to operate on"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Output type (no schema validation to allow error returns)
|
|
19
|
+
interface GitOutput {
|
|
20
|
+
success: boolean;
|
|
21
|
+
output: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const gitTool = createTool({
|
|
26
|
+
id: "git",
|
|
27
|
+
description: `Execute read-only git commands to analyze repository history and changes.
|
|
28
|
+
Available commands: ${ALLOWED_COMMANDS.join(", ")}.
|
|
29
|
+
Use this to investigate recent changes that might have caused issues.
|
|
30
|
+
Examples:
|
|
31
|
+
- Recent commits: { command: "log", args: ["--oneline", "-n", "20"] }
|
|
32
|
+
- Changes in last 5 commits: { command: "diff", args: ["HEAD~5..HEAD"] }
|
|
33
|
+
- Show specific commit: { command: "show", args: ["abc123"] }
|
|
34
|
+
- Blame a file: { command: "blame", path: "src/app.ts" }
|
|
35
|
+
- Log for specific file: { command: "log", args: ["-p", "-n", "5", "--"], path: "src/api/handler.ts" }`,
|
|
36
|
+
|
|
37
|
+
inputSchema: GitInputSchema,
|
|
38
|
+
|
|
39
|
+
execute: async (inputData): Promise<GitOutput> => {
|
|
40
|
+
const { command, args, path } = inputData;
|
|
41
|
+
|
|
42
|
+
// Build git command
|
|
43
|
+
const parts = ["git", command];
|
|
44
|
+
|
|
45
|
+
if (args && args.length > 0) {
|
|
46
|
+
parts.push(...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (path) {
|
|
50
|
+
// Ensure path separator for git commands that need it
|
|
51
|
+
if (command === "log" && !args?.includes("--")) {
|
|
52
|
+
parts.push("--");
|
|
53
|
+
}
|
|
54
|
+
parts.push(path);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fullCommand = parts.join(" ");
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await execCommand(fullCommand);
|
|
61
|
+
|
|
62
|
+
if (result.exitCode !== 0) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
output: "",
|
|
66
|
+
error: result.stderr || `Command failed with exit code ${result.exitCode}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
output: result.stdout,
|
|
73
|
+
error: result.stderr || undefined,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
output: "",
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Bashlet } from "@bashlet/sdk";
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { readFile as fsReadFile, readdir } from "fs/promises";
|
|
4
|
+
import type { Config } from "../config.js";
|
|
5
|
+
|
|
6
|
+
export interface CommandResult {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SandboxOptions {
|
|
13
|
+
codebasePath: string;
|
|
14
|
+
kubeConfigPath: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
useHost?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let bashletInstance: Bashlet | null = null;
|
|
20
|
+
let hostMode = false;
|
|
21
|
+
let hostWorkdir = "./";
|
|
22
|
+
|
|
23
|
+
export function createSandbox(options: SandboxOptions): void {
|
|
24
|
+
hostMode = options.useHost ?? false;
|
|
25
|
+
hostWorkdir = options.codebasePath;
|
|
26
|
+
|
|
27
|
+
if (hostMode) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (bashletInstance) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bashletInstance = new Bashlet({
|
|
36
|
+
mounts: [
|
|
37
|
+
{ hostPath: options.codebasePath, guestPath: "/workspace" },
|
|
38
|
+
{ hostPath: options.kubeConfigPath, guestPath: "/root/.kube" },
|
|
39
|
+
],
|
|
40
|
+
workdir: "/workspace",
|
|
41
|
+
timeout: options.timeout || 120,
|
|
42
|
+
envVars: [
|
|
43
|
+
{ key: "KUBECONFIG", value: "/root/.kube/config" },
|
|
44
|
+
{ key: "HOME", value: "/root" },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isHostMode(): boolean {
|
|
50
|
+
return hostMode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function execCommand(command: string): Promise<CommandResult> {
|
|
54
|
+
if (hostMode) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await $`sh -c ${command}`.cwd(hostWorkdir).nothrow().quiet();
|
|
57
|
+
return {
|
|
58
|
+
stdout: result.stdout.toString(),
|
|
59
|
+
stderr: result.stderr.toString(),
|
|
60
|
+
exitCode: result.exitCode,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
stdout: "",
|
|
65
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
66
|
+
exitCode: 1,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!bashletInstance) {
|
|
72
|
+
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await bashletInstance.exec(command);
|
|
77
|
+
return {
|
|
78
|
+
stdout: result.stdout || "",
|
|
79
|
+
stderr: result.stderr || "",
|
|
80
|
+
exitCode: result.exitCode ?? 0,
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
stdout: "",
|
|
85
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
86
|
+
exitCode: 1,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function readFile(path: string): Promise<string> {
|
|
92
|
+
if (hostMode) {
|
|
93
|
+
const fullPath = path.startsWith("/") ? path : `${hostWorkdir}/${path}`;
|
|
94
|
+
try {
|
|
95
|
+
return await fsReadFile(fullPath, "utf-8");
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Failed to read file ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!bashletInstance) {
|
|
104
|
+
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = await bashletInstance.readFile(path);
|
|
109
|
+
return content;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Failed to read file ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function listDir(path: string): Promise<string[]> {
|
|
118
|
+
if (hostMode) {
|
|
119
|
+
const fullPath = path.startsWith("/") ? path : `${hostWorkdir}/${path}`;
|
|
120
|
+
try {
|
|
121
|
+
const entries = await readdir(fullPath);
|
|
122
|
+
return entries;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Failed to list directory ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!bashletInstance) {
|
|
131
|
+
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const output = await bashletInstance.listDir(path);
|
|
136
|
+
return output.split("\n").filter((line) => line.trim().length > 0);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Failed to list directory ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function initSandboxFromConfig(config: Config, useHost: boolean = false): void {
|
|
145
|
+
createSandbox({
|
|
146
|
+
codebasePath: config.codebasePath,
|
|
147
|
+
kubeConfigPath: config.kubeConfigPath,
|
|
148
|
+
timeout: 120,
|
|
149
|
+
useHost,
|
|
150
|
+
});
|
|
151
|
+
}
|