@vfarcic/dot-ai 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/error-handling.js +1 -1
- package/dist/core/git-utils.d.ts +5 -0
- package/dist/core/git-utils.d.ts.map +1 -1
- package/dist/core/git-utils.js +16 -0
- package/dist/core/internal-tools.d.ts +35 -0
- package/dist/core/internal-tools.d.ts.map +1 -0
- package/dist/core/internal-tools.js +286 -0
- package/dist/interfaces/mcp.js +1 -1
- package/dist/interfaces/oauth/middleware.d.ts.map +1 -1
- package/dist/interfaces/oauth/middleware.js +4 -2
- package/dist/tools/push-to-git.d.ts.map +1 -1
- package/dist/tools/push-to-git.js +1 -11
- package/dist/tools/remediate.d.ts +9 -0
- package/dist/tools/remediate.d.ts.map +1 -1
- package/dist/tools/remediate.js +34 -9
- package/package.json +1 -1
- package/prompts/operate-system.md +3 -3
- package/prompts/remediate-system.md +62 -2
|
@@ -109,7 +109,7 @@ class ConsoleLogger {
|
|
|
109
109
|
const timestamp = new Date().toISOString();
|
|
110
110
|
const baseMessage = `[${timestamp}] ${level.toUpperCase()} [${this.component}] ${message}`;
|
|
111
111
|
if (data) {
|
|
112
|
-
return `${baseMessage} ${JSON.stringify(data
|
|
112
|
+
return `${baseMessage} ${JSON.stringify(data)}`;
|
|
113
113
|
}
|
|
114
114
|
return baseMessage;
|
|
115
115
|
}
|
package/dist/core/git-utils.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export declare function scrubCredentials(message: string): string;
|
|
|
23
23
|
export declare function getAuthenticatedUrl(repoUrl: string, token: string): string;
|
|
24
24
|
export declare function getAuthToken(authConfig: GitAuthConfig): Promise<string>;
|
|
25
25
|
export declare function getGitAuthConfigFromEnv(): GitAuthConfig;
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize a relative path to prevent directory traversal.
|
|
28
|
+
* Rejects absolute paths and paths that escape the base directory.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sanitizeRelativePath(relativePath: string): string;
|
|
26
31
|
export interface CloneOptions {
|
|
27
32
|
branch?: string;
|
|
28
33
|
depth?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../../src/core/git-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AASD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1E;AA2ED,wBAAsB,YAAY,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E;AAED,wBAAgB,uBAAuB,IAAI,aAAa,CAyBvD;AAeD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BhD;AAID,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B5E;AAID,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,UAAU,CAAC,CAyErB"}
|
|
1
|
+
{"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../../src/core/git-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AASD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1E;AA2ED,wBAAsB,YAAY,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E;AAED,wBAAgB,uBAAuB,IAAI,aAAa,CAyBvD;AAeD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CASjE;AAID,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BhD;AAID,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B5E;AAID,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,UAAU,CAAC,CAyErB"}
|
package/dist/core/git-utils.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.scrubCredentials = scrubCredentials;
|
|
|
53
53
|
exports.getAuthenticatedUrl = getAuthenticatedUrl;
|
|
54
54
|
exports.getAuthToken = getAuthToken;
|
|
55
55
|
exports.getGitAuthConfigFromEnv = getGitAuthConfigFromEnv;
|
|
56
|
+
exports.sanitizeRelativePath = sanitizeRelativePath;
|
|
56
57
|
exports.cloneRepo = cloneRepo;
|
|
57
58
|
exports.pullRepo = pullRepo;
|
|
58
59
|
exports.pushRepo = pushRepo;
|
|
@@ -161,6 +162,21 @@ function gitOptions(baseDir) {
|
|
|
161
162
|
timeout: { block: GIT_TIMEOUT_MS },
|
|
162
163
|
};
|
|
163
164
|
}
|
|
165
|
+
// ─── Path safety ───
|
|
166
|
+
/**
|
|
167
|
+
* Sanitize a relative path to prevent directory traversal.
|
|
168
|
+
* Rejects absolute paths and paths that escape the base directory.
|
|
169
|
+
*/
|
|
170
|
+
function sanitizeRelativePath(relativePath) {
|
|
171
|
+
if (relativePath.startsWith('/')) {
|
|
172
|
+
throw new Error('Relative path cannot be absolute');
|
|
173
|
+
}
|
|
174
|
+
const normalized = path.posix.normalize(relativePath);
|
|
175
|
+
if (normalized.startsWith('..') || path.posix.isAbsolute(normalized)) {
|
|
176
|
+
throw new Error('Relative path cannot escape target directory');
|
|
177
|
+
}
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
164
180
|
async function cloneRepo(repoUrl, targetDir, opts) {
|
|
165
181
|
const authConfig = getGitAuthConfigFromEnv();
|
|
166
182
|
let cloneUrl;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal Agentic-Loop Tools (PRD #407)
|
|
3
|
+
*
|
|
4
|
+
* Tools that run locally in the MCP server, available to the AI
|
|
5
|
+
* during investigation loops alongside plugin tools. NOT exposed
|
|
6
|
+
* to client agents — only the AI inside toolLoop() calls these.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - git_clone: Clone a Git repo
|
|
10
|
+
* - fs_list: List files at a path
|
|
11
|
+
* - fs_read: Read a file at a path
|
|
12
|
+
*
|
|
13
|
+
* All filesystem operations are scoped to ./tmp/gitops-clones/
|
|
14
|
+
* to prevent path traversal attacks.
|
|
15
|
+
*/
|
|
16
|
+
import type { AITool, ToolExecutor } from './ai-provider.interface.js';
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a relative path is safe and resolve it within the clones directory.
|
|
19
|
+
* Uses sanitizeRelativePath for traversal checks, then resolves to absolute.
|
|
20
|
+
* Exported for testing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validatePathWithinClones(inputPath: string): string;
|
|
23
|
+
export declare function getInternalTools(): AITool[];
|
|
24
|
+
/**
|
|
25
|
+
* Create a ToolExecutor that handles internal agentic-loop tools.
|
|
26
|
+
* Designed to be passed as the fallbackExecutor to pluginManager.createToolExecutor().
|
|
27
|
+
*/
|
|
28
|
+
export declare function createInternalToolExecutor(sessionId: string): ToolExecutor;
|
|
29
|
+
/**
|
|
30
|
+
* Remove session clone directories older than the TTL.
|
|
31
|
+
* Called at the start of each new remediate investigation.
|
|
32
|
+
* Non-blocking: runs cleanup in the background without delaying investigation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function cleanupOldClones(maxAgeMs?: number): void;
|
|
35
|
+
//# sourceMappingURL=internal-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-tools.d.ts","sourceRoot":"","sources":["../../src/core/internal-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAcvE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAUlE;AAkBD,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAgD3C;AAmHD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAoB1E;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,MAAuB,GAAG,IAAI,CAoBxE"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Internal Agentic-Loop Tools (PRD #407)
|
|
4
|
+
*
|
|
5
|
+
* Tools that run locally in the MCP server, available to the AI
|
|
6
|
+
* during investigation loops alongside plugin tools. NOT exposed
|
|
7
|
+
* to client agents — only the AI inside toolLoop() calls these.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* - git_clone: Clone a Git repo
|
|
11
|
+
* - fs_list: List files at a path
|
|
12
|
+
* - fs_read: Read a file at a path
|
|
13
|
+
*
|
|
14
|
+
* All filesystem operations are scoped to ./tmp/gitops-clones/
|
|
15
|
+
* to prevent path traversal attacks.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.validatePathWithinClones = validatePathWithinClones;
|
|
52
|
+
exports.getInternalTools = getInternalTools;
|
|
53
|
+
exports.createInternalToolExecutor = createInternalToolExecutor;
|
|
54
|
+
exports.cleanupOldClones = cleanupOldClones;
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const git_utils_js_1 = require("./git-utils.js");
|
|
58
|
+
const solution_utils_js_1 = require("./solution-utils.js");
|
|
59
|
+
const CLONES_SUBDIR = 'gitops-clones';
|
|
60
|
+
const MAX_FILE_SIZE = 100 * 1024; // 100KB
|
|
61
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
62
|
+
// ─── Path security ───
|
|
63
|
+
function getClonesDir() {
|
|
64
|
+
return path.resolve(process.cwd(), 'tmp', CLONES_SUBDIR);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate that a relative path is safe and resolve it within the clones directory.
|
|
68
|
+
* Uses sanitizeRelativePath for traversal checks, then resolves to absolute.
|
|
69
|
+
* Exported for testing.
|
|
70
|
+
*/
|
|
71
|
+
function validatePathWithinClones(inputPath) {
|
|
72
|
+
// Decode URL-encoded characters (e.g., %2e%2e/ for ../) before validation
|
|
73
|
+
let decoded;
|
|
74
|
+
try {
|
|
75
|
+
decoded = decodeURIComponent(inputPath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
decoded = inputPath;
|
|
79
|
+
}
|
|
80
|
+
const sanitized = (0, git_utils_js_1.sanitizeRelativePath)(decoded);
|
|
81
|
+
return path.resolve(getClonesDir(), sanitized);
|
|
82
|
+
}
|
|
83
|
+
// ─── Repo name sanitization ───
|
|
84
|
+
function repoUrlToDirectoryName(repoUrl) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(repoUrl);
|
|
87
|
+
const repoPath = url.pathname
|
|
88
|
+
.replace(/^\//, '')
|
|
89
|
+
.replace(/\.git$/, '');
|
|
90
|
+
return (0, solution_utils_js_1.sanitizeIntentForLabel)(repoPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return (0, solution_utils_js_1.sanitizeIntentForLabel)(repoUrl.slice(0, 63));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ─── Tool definitions ───
|
|
97
|
+
function getInternalTools() {
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
name: 'git_clone',
|
|
101
|
+
description: 'Clone a Git repository. Returns a relative path to the cloned repo.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
repoUrl: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Repository URL (HTTPS)',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['repoUrl'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'fs_list',
|
|
115
|
+
description: 'List files and directories at a relative path within the working directory.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
path: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'Relative path to list',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ['path'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'fs_read',
|
|
129
|
+
description: 'Read file contents at a relative path within the working directory.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
path: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'Relative path to file',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['path'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
// ─── Handlers ───
|
|
144
|
+
async function handleGitClone(args, sessionId) {
|
|
145
|
+
const repoUrl = args.repoUrl;
|
|
146
|
+
if (!repoUrl) {
|
|
147
|
+
return 'Error: repoUrl is required';
|
|
148
|
+
}
|
|
149
|
+
const repoName = repoUrlToDirectoryName(repoUrl);
|
|
150
|
+
const relativePath = path.join(sessionId, repoName);
|
|
151
|
+
const targetDir = path.join(getClonesDir(), relativePath);
|
|
152
|
+
if (fs.existsSync(targetDir)) {
|
|
153
|
+
return { localPath: relativePath, message: 'Repository already cloned' };
|
|
154
|
+
}
|
|
155
|
+
const parentDir = path.dirname(targetDir);
|
|
156
|
+
if (!fs.existsSync(parentDir)) {
|
|
157
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const result = await (0, git_utils_js_1.cloneRepo)(repoUrl, targetDir, { depth: 1 });
|
|
161
|
+
return { localPath: relativePath, branch: result.branch };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
return `Error cloning repository: ${(0, git_utils_js_1.scrubCredentials)(message)}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function handleFsList(args) {
|
|
169
|
+
const inputPath = args.path;
|
|
170
|
+
if (!inputPath) {
|
|
171
|
+
return 'Error: path is required';
|
|
172
|
+
}
|
|
173
|
+
let resolved;
|
|
174
|
+
try {
|
|
175
|
+
resolved = validatePathWithinClones(inputPath);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
179
|
+
}
|
|
180
|
+
if (!fs.existsSync(resolved)) {
|
|
181
|
+
return `Error: path does not exist: ${inputPath}`;
|
|
182
|
+
}
|
|
183
|
+
const stat = fs.statSync(resolved);
|
|
184
|
+
if (!stat.isDirectory()) {
|
|
185
|
+
return `Error: path is not a directory: ${inputPath}`;
|
|
186
|
+
}
|
|
187
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
188
|
+
return entries.map((entry) => ({
|
|
189
|
+
name: entry.name,
|
|
190
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
function handleFsRead(args) {
|
|
194
|
+
const inputPath = args.path;
|
|
195
|
+
if (!inputPath) {
|
|
196
|
+
return 'Error: path is required';
|
|
197
|
+
}
|
|
198
|
+
let resolved;
|
|
199
|
+
try {
|
|
200
|
+
resolved = validatePathWithinClones(inputPath);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
204
|
+
}
|
|
205
|
+
if (!fs.existsSync(resolved)) {
|
|
206
|
+
return `Error: file does not exist: ${inputPath}`;
|
|
207
|
+
}
|
|
208
|
+
const stat = fs.statSync(resolved);
|
|
209
|
+
if (stat.isDirectory()) {
|
|
210
|
+
return `Error: path is a directory, not a file: ${inputPath}`;
|
|
211
|
+
}
|
|
212
|
+
// Binary detection: check first 8KB for null bytes
|
|
213
|
+
const detectBuffer = Buffer.alloc(Math.min(8192, stat.size));
|
|
214
|
+
const fd = fs.openSync(resolved, 'r');
|
|
215
|
+
try {
|
|
216
|
+
fs.readSync(fd, detectBuffer, 0, detectBuffer.length, 0);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
fs.closeSync(fd);
|
|
220
|
+
}
|
|
221
|
+
if (detectBuffer.includes(0)) {
|
|
222
|
+
return 'Binary file, cannot display';
|
|
223
|
+
}
|
|
224
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
225
|
+
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
|
226
|
+
const fd = fs.openSync(resolved, 'r');
|
|
227
|
+
try {
|
|
228
|
+
const bytesRead = fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, 0);
|
|
229
|
+
const content = buffer.toString('utf-8', 0, bytesRead);
|
|
230
|
+
return `${content}\n\n[Truncated: file exceeds ${MAX_FILE_SIZE / 1024}KB limit]`;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
fs.closeSync(fd);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return fs.readFileSync(resolved, 'utf-8');
|
|
237
|
+
}
|
|
238
|
+
// ─── Combined executor ───
|
|
239
|
+
/**
|
|
240
|
+
* Create a ToolExecutor that handles internal agentic-loop tools.
|
|
241
|
+
* Designed to be passed as the fallbackExecutor to pluginManager.createToolExecutor().
|
|
242
|
+
*/
|
|
243
|
+
function createInternalToolExecutor(sessionId) {
|
|
244
|
+
const handlers = {
|
|
245
|
+
git_clone: (args) => handleGitClone(args, sessionId),
|
|
246
|
+
fs_list: handleFsList,
|
|
247
|
+
fs_read: handleFsRead,
|
|
248
|
+
};
|
|
249
|
+
return async (toolName, input) => {
|
|
250
|
+
const handler = handlers[toolName];
|
|
251
|
+
if (!handler) {
|
|
252
|
+
return `Error: unknown internal tool: ${toolName}`;
|
|
253
|
+
}
|
|
254
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
255
|
+
return 'Error: tool input must be an object';
|
|
256
|
+
}
|
|
257
|
+
return handler(input);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ─── TTL cleanup ───
|
|
261
|
+
/**
|
|
262
|
+
* Remove session clone directories older than the TTL.
|
|
263
|
+
* Called at the start of each new remediate investigation.
|
|
264
|
+
* Non-blocking: runs cleanup in the background without delaying investigation.
|
|
265
|
+
*/
|
|
266
|
+
function cleanupOldClones(maxAgeMs = DEFAULT_TTL_MS) {
|
|
267
|
+
const clonesDir = getClonesDir();
|
|
268
|
+
fs.promises.access(clonesDir).then(async () => {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const entries = await fs.promises.readdir(clonesDir);
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const entryPath = path.join(clonesDir, entry);
|
|
273
|
+
try {
|
|
274
|
+
const stat = await fs.promises.stat(entryPath);
|
|
275
|
+
if (stat.isDirectory() && now - stat.mtimeMs > maxAgeMs) {
|
|
276
|
+
await fs.promises.rm(entryPath, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Ignore errors during cleanup (e.g., concurrent deletion)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}).catch(() => {
|
|
284
|
+
// Directory doesn't exist yet, nothing to clean
|
|
285
|
+
});
|
|
286
|
+
}
|
package/dist/interfaces/mcp.js
CHANGED
|
@@ -403,7 +403,7 @@ class MCPServer {
|
|
|
403
403
|
// Handle CORS for browser-based clients
|
|
404
404
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
405
405
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
406
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Session-Id, Authorization');
|
|
406
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Session-Id, Authorization, X-Dot-AI-Authorization');
|
|
407
407
|
if (req.method === 'OPTIONS') {
|
|
408
408
|
res.writeHead(204);
|
|
409
409
|
res.end();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/interfaces/oauth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/interfaces/oauth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,UAAU,CA4FhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
|
|
@@ -32,8 +32,10 @@ function checkBearerAuth(req) {
|
|
|
32
32
|
message: 'Authentication is not configured. Set DOT_AI_AUTH_TOKEN in your deployment.',
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
// Extract Authorization header
|
|
36
|
-
|
|
35
|
+
// Extract Authorization header, with X-Dot-AI-Authorization as fallback.
|
|
36
|
+
// The fallback supports Kubernetes API server proxy scenarios where the
|
|
37
|
+
// standard Authorization header is overwritten with a K8s bearer token.
|
|
38
|
+
const rawAuthHeader = req.headers['x-dot-ai-authorization'] || req.headers['authorization'];
|
|
37
39
|
if (!rawAuthHeader) {
|
|
38
40
|
return {
|
|
39
41
|
authorized: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push-to-git.d.ts","sourceRoot":"","sources":["../../src/tools/push-to-git.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EAAE,KAAK,EAA0B,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"push-to-git.d.ts","sourceRoot":"","sources":["../../src/tools/push-to-git.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EAAE,KAAK,EAA0B,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,0BAA0B,0HACkF,CAAC;AAE1H,eAAO,MAAM,2BAA2B;;;;;;;;;CAwBvC,CAAC;AAEF,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAGD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,cAAc,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,GACnD,OAAO,CAAC;IAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAC,CAiUxD"}
|
|
@@ -76,16 +76,6 @@ exports.PUSHTOGIT_TOOL_INPUT_SCHEMA = {
|
|
|
76
76
|
.optional()
|
|
77
77
|
.describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.'),
|
|
78
78
|
};
|
|
79
|
-
function sanitizeRelativePath(relativePath) {
|
|
80
|
-
if (relativePath.startsWith('/')) {
|
|
81
|
-
throw new Error('Relative path cannot be absolute');
|
|
82
|
-
}
|
|
83
|
-
const normalized = path.posix.normalize(relativePath);
|
|
84
|
-
if (normalized.startsWith('..') || path.posix.isAbsolute(normalized)) {
|
|
85
|
-
throw new Error('Relative path cannot escape target directory');
|
|
86
|
-
}
|
|
87
|
-
return normalized;
|
|
88
|
-
}
|
|
89
79
|
async function handlePushToGitTool(args, dotAI, logger, requestId, sessionManager) {
|
|
90
80
|
return await error_handling_1.ErrorHandler.withErrorHandling(async () => {
|
|
91
81
|
logger.info('Handling pushToGit request', {
|
|
@@ -204,7 +194,7 @@ async function handlePushToGitTool(args, dotAI, logger, requestId, sessionManage
|
|
|
204
194
|
const manifestFiles = solution.generatedManifests.files;
|
|
205
195
|
if (manifestFiles && manifestFiles.length > 0) {
|
|
206
196
|
for (const file of manifestFiles) {
|
|
207
|
-
const sanitizedPath = sanitizeRelativePath(file.relativePath);
|
|
197
|
+
const sanitizedPath = (0, git_utils_1.sanitizeRelativePath)(file.relativePath);
|
|
208
198
|
files.push({
|
|
209
199
|
path: path.posix.join(targetPath, sanitizedPath),
|
|
210
200
|
content: file.content,
|
|
@@ -52,6 +52,15 @@ export interface RemediationAction {
|
|
|
52
52
|
command?: string;
|
|
53
53
|
risk: 'low' | 'medium' | 'high';
|
|
54
54
|
rationale: string;
|
|
55
|
+
gitSource?: {
|
|
56
|
+
repoURL: string;
|
|
57
|
+
branch: string;
|
|
58
|
+
files: Array<{
|
|
59
|
+
path: string;
|
|
60
|
+
content: string;
|
|
61
|
+
description: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
55
64
|
}
|
|
56
65
|
export interface ExecutionResult {
|
|
57
66
|
action: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remediate.d.ts","sourceRoot":"","sources":["../../src/tools/remediate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,OAAO,EAEL,qBAAqB,EACtB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"remediate.d.ts","sourceRoot":"","sources":["../../src/tools/remediate.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/remediate.js
CHANGED
|
@@ -50,6 +50,7 @@ const fs = __importStar(require("fs"));
|
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
51
|
const request_context_1 = require("../interfaces/request-context");
|
|
52
52
|
const rbac_1 = require("../core/rbac");
|
|
53
|
+
const internal_tools_1 = require("../core/internal-tools");
|
|
53
54
|
// PRD #143 Milestone 1: Hybrid approach - AI can use kubectl_api_resources tool OR continue with JSON dataRequests
|
|
54
55
|
// Tool metadata for direct MCP registration
|
|
55
56
|
exports.REMEDIATE_TOOL_NAME = 'remediate';
|
|
@@ -146,15 +147,20 @@ async function conductInvestigation(session, sessionManager, aiProvider, logger,
|
|
|
146
147
|
if (kubectlTools.length === 0) {
|
|
147
148
|
throw new Error('No kubectl tools available from plugin. Ensure agentic-tools plugin is running.');
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
// PRD #407: Combine plugin tools (allowlisted) with internal tools (git_clone, fs_list, fs_read)
|
|
151
|
+
const investigationTools = [...kubectlTools, ...(0, internal_tools_1.getInternalTools)()];
|
|
152
|
+
logger.debug('Starting toolLoop with investigation tools', {
|
|
150
153
|
requestId,
|
|
151
154
|
sessionId: session.sessionId,
|
|
152
|
-
toolCount:
|
|
153
|
-
tools:
|
|
155
|
+
toolCount: investigationTools.length,
|
|
156
|
+
tools: investigationTools.map(t => t.name),
|
|
154
157
|
});
|
|
155
|
-
// PRD #
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
+
// PRD #407: Clean up old clone directories (non-blocking)
|
|
159
|
+
(0, internal_tools_1.cleanupOldClones)();
|
|
160
|
+
// PRD #407: Combined executor routes plugin tools to plugin, internal tools to local handlers
|
|
161
|
+
const internalExecutor = (0, internal_tools_1.createInternalToolExecutor)(session.sessionId);
|
|
162
|
+
const toolExecutor = pluginManager.createToolExecutor(internalExecutor);
|
|
163
|
+
// Use toolLoop for AI-driven investigation with all investigation tools
|
|
158
164
|
// System prompt is static (cached), issue description is dynamic (userMessage)
|
|
159
165
|
const operationName = isValidation
|
|
160
166
|
? 'remediate-validation'
|
|
@@ -162,7 +168,7 @@ async function conductInvestigation(session, sessionManager, aiProvider, logger,
|
|
|
162
168
|
const result = await aiProvider.toolLoop({
|
|
163
169
|
systemPrompt: systemPrompt,
|
|
164
170
|
userMessage: `Investigate this Kubernetes issue: ${session.data.issue}`,
|
|
165
|
-
tools:
|
|
171
|
+
tools: investigationTools,
|
|
166
172
|
toolExecutor: toolExecutor,
|
|
167
173
|
maxIterations: maxIterations,
|
|
168
174
|
operation: operationName,
|
|
@@ -436,6 +442,7 @@ async function executeRemediationCommands(session, sessionManager, logger, reque
|
|
|
436
442
|
const results = [];
|
|
437
443
|
const finalAnalysis = session.data.finalAnalysis;
|
|
438
444
|
let overallSuccess = true;
|
|
445
|
+
let executedCommandCount = 0;
|
|
439
446
|
logger.info('Starting remediation command execution', {
|
|
440
447
|
requestId,
|
|
441
448
|
sessionId: session.sessionId,
|
|
@@ -446,6 +453,23 @@ async function executeRemediationCommands(session, sessionManager, logger, reque
|
|
|
446
453
|
const action = finalAnalysis.remediation.actions[i];
|
|
447
454
|
const actionId = `action_${i + 1}`;
|
|
448
455
|
try {
|
|
456
|
+
// PRD #407: Skip gitSource actions — these are Git-based remediation
|
|
457
|
+
// instructions for GitOps-managed resources, not executable commands
|
|
458
|
+
if (action.gitSource && !action.command) {
|
|
459
|
+
logger.info('Skipping gitSource remediation action (not executable)', {
|
|
460
|
+
requestId,
|
|
461
|
+
sessionId: session.sessionId,
|
|
462
|
+
actionId,
|
|
463
|
+
repoURL: action.gitSource.repoURL,
|
|
464
|
+
});
|
|
465
|
+
results.push({
|
|
466
|
+
action: `${actionId}: ${action.description} (skipped: Git-based)`,
|
|
467
|
+
success: false,
|
|
468
|
+
output: 'Git-based remediation — apply changes in the source repository',
|
|
469
|
+
timestamp: new Date(),
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
449
473
|
logger.info('Executing remediation action', {
|
|
450
474
|
requestId,
|
|
451
475
|
sessionId: session.sessionId,
|
|
@@ -486,6 +510,7 @@ async function executeRemediationCommands(session, sessionManager, logger, reque
|
|
|
486
510
|
else {
|
|
487
511
|
output = String(response.result || '');
|
|
488
512
|
}
|
|
513
|
+
executedCommandCount++;
|
|
489
514
|
results.push({
|
|
490
515
|
action: `${actionId}: ${action.description}`,
|
|
491
516
|
success: true,
|
|
@@ -515,9 +540,9 @@ async function executeRemediationCommands(session, sessionManager, logger, reque
|
|
|
515
540
|
});
|
|
516
541
|
}
|
|
517
542
|
}
|
|
518
|
-
// Run automatic post-execution validation if all
|
|
543
|
+
// Run automatic post-execution validation if commands were executed and all succeeded
|
|
519
544
|
let validationResult = null;
|
|
520
|
-
if (overallSuccess && finalAnalysis.validationIntent) {
|
|
545
|
+
if (overallSuccess && executedCommandCount > 0 && finalAnalysis.validationIntent) {
|
|
521
546
|
const validationIntent = finalAnalysis.validationIntent;
|
|
522
547
|
try {
|
|
523
548
|
// In automatic mode, wait for Kubernetes to apply changes before validating
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vfarcic/dot-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance",
|
|
5
5
|
"mcpName": "io.github.vfarcic/dot-ai",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -43,11 +43,11 @@ You help users perform Day 2 operations on Kubernetes applications through natur
|
|
|
43
43
|
|
|
44
44
|
**Validation workflow**:
|
|
45
45
|
1. Design your operational solution
|
|
46
|
-
2. Generate kubectl commands
|
|
46
|
+
2. Generate the exact kubectl commands you intend to propose
|
|
47
47
|
3. Run dry-run validation for each command
|
|
48
|
-
4. If validation fails, analyze the error
|
|
48
|
+
4. If validation fails, analyze the error, fix the command, and re-validate
|
|
49
49
|
5. Retry validation until all commands pass
|
|
50
|
-
6.
|
|
50
|
+
6. Copy the validated commands verbatim into your final JSON response — the commands array **MUST be identical** to what passed dry-run. If you modify a command after validation (even slightly), you MUST re-validate it.
|
|
51
51
|
|
|
52
52
|
**Multiple iterations are expected**: You may need several dry-run attempts to get manifests correct (schema issues, field names, API versions). This is normal and expected.
|
|
53
53
|
|
|
@@ -59,9 +59,20 @@ Once investigation is complete, respond with ONLY this JSON format:
|
|
|
59
59
|
"actions": [
|
|
60
60
|
{
|
|
61
61
|
"description": "Specific action to take",
|
|
62
|
-
"command": "command to execute (kubectl, helm, etc.)",
|
|
62
|
+
"command": "command to execute (kubectl, helm, etc.) — omit if gitSource is provided",
|
|
63
63
|
"risk": "low|medium|high",
|
|
64
|
-
"rationale": "Why this action addresses the issue"
|
|
64
|
+
"rationale": "Why this action addresses the issue",
|
|
65
|
+
"gitSource": {
|
|
66
|
+
"repoURL": "source repository URL — only when resource is GitOps-managed",
|
|
67
|
+
"branch": "branch name",
|
|
68
|
+
"files": [
|
|
69
|
+
{
|
|
70
|
+
"path": "path relative to repo root",
|
|
71
|
+
"content": "full corrected file content",
|
|
72
|
+
"description": "what was changed and why"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
],
|
|
67
78
|
"risk": "low|medium|high"
|
|
@@ -88,6 +99,17 @@ Once investigation is complete, respond with ONLY this JSON format:
|
|
|
88
99
|
- All components healthy
|
|
89
100
|
- Set `actions: []` and explain why no issue found
|
|
90
101
|
|
|
102
|
+
## GitOps Awareness
|
|
103
|
+
|
|
104
|
+
After identifying the problematic resource, check whether it is managed by a GitOps controller (e.g., Argo CD, Flux).
|
|
105
|
+
|
|
106
|
+
**When GitOps management is detected**:
|
|
107
|
+
- Clone the source repo, navigate and read the manifests to find the file(s) that need changing
|
|
108
|
+
- Include `gitSource` in your remediation actions with the repo URL, branch, and full corrected file contents for each file that needs modification
|
|
109
|
+
|
|
110
|
+
**When GitOps management is NOT detected**:
|
|
111
|
+
- Proceed with standard kubectl-based remediation
|
|
112
|
+
|
|
91
113
|
### Remediation Action Guidelines
|
|
92
114
|
|
|
93
115
|
**Structure your solution efficiently**:
|
|
@@ -146,6 +168,44 @@ Once investigation is complete, respond with ONLY this JSON format:
|
|
|
146
168
|
}
|
|
147
169
|
```
|
|
148
170
|
|
|
171
|
+
## Example Response - GitOps-Managed Resource
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"issueStatus": "active",
|
|
176
|
+
"rootCause": "Deployment 'api-server' has invalid image tag 'v2.broken' causing CrashLoopBackOff",
|
|
177
|
+
"confidence": 0.95,
|
|
178
|
+
"factors": [
|
|
179
|
+
"Pod is in CrashLoopBackOff with ImagePullBackOff events",
|
|
180
|
+
"Image tag 'v2.broken' does not exist in registry",
|
|
181
|
+
"Resource is managed by Argo CD Application 'api-server'"
|
|
182
|
+
],
|
|
183
|
+
"remediation": {
|
|
184
|
+
"summary": "Update image tag in Git source to valid version",
|
|
185
|
+
"actions": [
|
|
186
|
+
{
|
|
187
|
+
"description": "Fix image tag in deployment manifest",
|
|
188
|
+
"risk": "low",
|
|
189
|
+
"rationale": "Changing image tag to latest stable version resolves the ImagePullBackOff. Argo CD will sync the change automatically.",
|
|
190
|
+
"gitSource": {
|
|
191
|
+
"repoURL": "https://github.com/org/infra-repo.git",
|
|
192
|
+
"branch": "main",
|
|
193
|
+
"files": [
|
|
194
|
+
{
|
|
195
|
+
"path": "apps/production/deployment.yaml",
|
|
196
|
+
"content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: api-server\nspec:\n template:\n spec:\n containers:\n - name: api-server\n image: org/api-server:v2.1.0\n",
|
|
197
|
+
"description": "Changed image tag from 'v2.broken' to 'v2.1.0'"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"risk": "low"
|
|
204
|
+
},
|
|
205
|
+
"validationIntent": "Wait for Argo CD to sync, then verify pods are running with the correct image"
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
149
209
|
## Example Response - No Issue Found
|
|
150
210
|
|
|
151
211
|
```json
|