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.
@@ -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
+ }