agileflow 2.89.2 → 2.90.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/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.90.0] - 2026-01-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- TUI dashboard with session management and modular CLI architecture
|
|
14
|
+
|
|
15
|
+
## [2.89.3] - 2026-01-14
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Security hardening with shell injection prevention and secret redaction
|
|
19
|
+
|
|
10
20
|
## [2.89.2] - 2026-01-14
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](docs/04-architecture/commands.md)
|
|
7
7
|
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](docs/04-architecture/skills.md)
|
|
9
9
|
|
|
@@ -65,7 +65,7 @@ AgileFlow combines three proven methodologies:
|
|
|
65
65
|
|
|
66
66
|
| Component | Count | Description |
|
|
67
67
|
|-----------|-------|-------------|
|
|
68
|
-
| [Commands](docs/04-architecture/commands.md) |
|
|
68
|
+
| [Commands](docs/04-architecture/commands.md) | 73 | Slash commands for agile workflows |
|
|
69
69
|
| [Agents/Experts](docs/04-architecture/subagents.md) | 29 | Specialized agents with self-improving knowledge bases |
|
|
70
70
|
| [Skills](docs/04-architecture/skills.md) | Dynamic | Generated on-demand with `/agileflow:skill:create` |
|
|
71
71
|
|
|
@@ -76,7 +76,7 @@ AgileFlow combines three proven methodologies:
|
|
|
76
76
|
Full documentation lives in [`docs/04-architecture/`](docs/04-architecture/):
|
|
77
77
|
|
|
78
78
|
### Reference
|
|
79
|
-
- [Commands](docs/04-architecture/commands.md) - All
|
|
79
|
+
- [Commands](docs/04-architecture/commands.md) - All 73 slash commands
|
|
80
80
|
- [Agents/Experts](docs/04-architecture/subagents.md) - 29 specialized agents with self-improving knowledge
|
|
81
81
|
- [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
|
|
82
82
|
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content-sanitizer.js - Content Security for Dynamic Injection
|
|
3
|
+
*
|
|
4
|
+
* Provides sanitization for dynamic content that gets injected into
|
|
5
|
+
* files during installation. Prevents injection attacks through
|
|
6
|
+
* malicious placeholder values.
|
|
7
|
+
*
|
|
8
|
+
* Security Model:
|
|
9
|
+
* - All dynamic values are validated against expected patterns
|
|
10
|
+
* - Special characters are escaped for Markdown and shell contexts
|
|
11
|
+
* - Agent/command names are restricted to safe character sets
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const { sanitize, validatePlaceholderValue } = require('./content-sanitizer');
|
|
15
|
+
*
|
|
16
|
+
* // Sanitize agent name for markdown
|
|
17
|
+
* const safeName = sanitize.name(agentName);
|
|
18
|
+
*
|
|
19
|
+
* // Validate count values
|
|
20
|
+
* const safeCount = sanitize.count(count);
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Patterns for validating dynamic content
|
|
25
|
+
*/
|
|
26
|
+
const PATTERNS = {
|
|
27
|
+
// Agent/command names: alphanumeric, hyphens, underscores
|
|
28
|
+
name: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
|
29
|
+
|
|
30
|
+
// Description: printable ASCII, no control chars or dangerous sequences
|
|
31
|
+
description: /^[\x20-\x7E\u00A0-\uFFFF]*$/,
|
|
32
|
+
|
|
33
|
+
// Count: non-negative integer
|
|
34
|
+
count: /^\d+$/,
|
|
35
|
+
|
|
36
|
+
// Version: semver-like pattern
|
|
37
|
+
version: /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?$/,
|
|
38
|
+
|
|
39
|
+
// Date: ISO date format
|
|
40
|
+
date: /^\d{4}-\d{2}-\d{2}$/,
|
|
41
|
+
|
|
42
|
+
// Folder name: alphanumeric, dots, hyphens, underscores
|
|
43
|
+
folderName: /^[a-zA-Z0-9._-]+$/,
|
|
44
|
+
|
|
45
|
+
// Tool name: alphanumeric with some special chars
|
|
46
|
+
toolName: /^[a-zA-Z][a-zA-Z0-9_*-]*$/,
|
|
47
|
+
|
|
48
|
+
// Model name: simple identifier
|
|
49
|
+
modelName: /^[a-z][a-z0-9-]*$/,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum lengths for various content types
|
|
54
|
+
*/
|
|
55
|
+
const MAX_LENGTHS = {
|
|
56
|
+
name: 64,
|
|
57
|
+
description: 500,
|
|
58
|
+
version: 32,
|
|
59
|
+
folderName: 64,
|
|
60
|
+
toolName: 32,
|
|
61
|
+
modelName: 32,
|
|
62
|
+
agentListEntry: 1000,
|
|
63
|
+
commandListEntry: 500,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Characters that need escaping in Markdown
|
|
68
|
+
*/
|
|
69
|
+
const MARKDOWN_ESCAPE_CHARS = [
|
|
70
|
+
'\\',
|
|
71
|
+
'`',
|
|
72
|
+
'*',
|
|
73
|
+
'_',
|
|
74
|
+
'{',
|
|
75
|
+
'}',
|
|
76
|
+
'[',
|
|
77
|
+
']',
|
|
78
|
+
'(',
|
|
79
|
+
')',
|
|
80
|
+
'#',
|
|
81
|
+
'+',
|
|
82
|
+
'-',
|
|
83
|
+
'.',
|
|
84
|
+
'!',
|
|
85
|
+
'|',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Characters that need escaping in shell context
|
|
90
|
+
*/
|
|
91
|
+
const SHELL_ESCAPE_CHARS = [
|
|
92
|
+
'$',
|
|
93
|
+
'`',
|
|
94
|
+
'\\',
|
|
95
|
+
'"',
|
|
96
|
+
"'",
|
|
97
|
+
'!',
|
|
98
|
+
'&',
|
|
99
|
+
';',
|
|
100
|
+
'|',
|
|
101
|
+
'>',
|
|
102
|
+
'<',
|
|
103
|
+
'(',
|
|
104
|
+
')',
|
|
105
|
+
'{',
|
|
106
|
+
'}',
|
|
107
|
+
'[',
|
|
108
|
+
']',
|
|
109
|
+
'*',
|
|
110
|
+
'?',
|
|
111
|
+
'#',
|
|
112
|
+
'~',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Escape special characters for Markdown content
|
|
117
|
+
* @param {string} text - Text to escape
|
|
118
|
+
* @returns {string} Escaped text safe for Markdown
|
|
119
|
+
*/
|
|
120
|
+
function escapeMarkdown(text) {
|
|
121
|
+
if (!text || typeof text !== 'string') return '';
|
|
122
|
+
|
|
123
|
+
let escaped = text;
|
|
124
|
+
for (const char of MARKDOWN_ESCAPE_CHARS) {
|
|
125
|
+
escaped = escaped.replace(new RegExp('\\' + char, 'g'), '\\' + char);
|
|
126
|
+
}
|
|
127
|
+
return escaped;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Escape special characters for shell/command context
|
|
132
|
+
* @param {string} text - Text to escape
|
|
133
|
+
* @returns {string} Escaped text safe for shell
|
|
134
|
+
*/
|
|
135
|
+
function escapeShell(text) {
|
|
136
|
+
if (!text || typeof text !== 'string') return '';
|
|
137
|
+
|
|
138
|
+
// Use a single regex replacement to avoid double-escaping backslashes
|
|
139
|
+
return text.replace(/[$`\\"'!&;|><(){}[\]*?#~]/g, char => '\\' + char);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove control characters from text
|
|
144
|
+
* @param {string} text - Text to clean
|
|
145
|
+
* @returns {string} Text without control characters
|
|
146
|
+
*/
|
|
147
|
+
function removeControlChars(text) {
|
|
148
|
+
if (!text || typeof text !== 'string') return '';
|
|
149
|
+
// Remove ASCII control chars (0x00-0x1F except tab/newline/carriage return)
|
|
150
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Truncate text to maximum length with ellipsis
|
|
155
|
+
* @param {string} text - Text to truncate
|
|
156
|
+
* @param {number} maxLength - Maximum length
|
|
157
|
+
* @returns {string} Truncated text
|
|
158
|
+
*/
|
|
159
|
+
function truncate(text, maxLength) {
|
|
160
|
+
if (!text || typeof text !== 'string') return '';
|
|
161
|
+
if (text.length <= maxLength) return text;
|
|
162
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Sanitization functions for different content types
|
|
167
|
+
*/
|
|
168
|
+
const sanitize = {
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize a name (agent name, command name, etc.)
|
|
171
|
+
* @param {string} name - Name to sanitize
|
|
172
|
+
* @param {Object} [options={}] - Options
|
|
173
|
+
* @param {boolean} [options.strict=true] - Strict validation (returns empty on invalid)
|
|
174
|
+
* @returns {string} Sanitized name
|
|
175
|
+
*/
|
|
176
|
+
name(name, options = {}) {
|
|
177
|
+
const { strict = true } = options;
|
|
178
|
+
|
|
179
|
+
if (!name || typeof name !== 'string') return '';
|
|
180
|
+
|
|
181
|
+
// Clean the name
|
|
182
|
+
let cleaned = removeControlChars(name).trim();
|
|
183
|
+
cleaned = truncate(cleaned, MAX_LENGTHS.name);
|
|
184
|
+
|
|
185
|
+
// Strict mode: validate against pattern
|
|
186
|
+
if (strict) {
|
|
187
|
+
if (!PATTERNS.name.test(cleaned)) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Permissive mode: just remove dangerous chars
|
|
192
|
+
cleaned = cleaned.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
193
|
+
// Ensure starts with letter
|
|
194
|
+
if (!/^[a-zA-Z]/.test(cleaned)) {
|
|
195
|
+
cleaned = 'x-' + cleaned;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return cleaned;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sanitize a description
|
|
204
|
+
* @param {string} description - Description to sanitize
|
|
205
|
+
* @param {Object} [options={}] - Options
|
|
206
|
+
* @param {boolean} [options.escapeMarkdown=true] - Escape markdown chars
|
|
207
|
+
* @returns {string} Sanitized description
|
|
208
|
+
*/
|
|
209
|
+
description(description, options = {}) {
|
|
210
|
+
const { escapeMarkdown: shouldEscape = true } = options;
|
|
211
|
+
|
|
212
|
+
if (!description || typeof description !== 'string') return '';
|
|
213
|
+
|
|
214
|
+
let cleaned = removeControlChars(description).trim();
|
|
215
|
+
cleaned = truncate(cleaned, MAX_LENGTHS.description);
|
|
216
|
+
|
|
217
|
+
if (shouldEscape) {
|
|
218
|
+
cleaned = escapeMarkdown(cleaned);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return cleaned;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sanitize a count value
|
|
226
|
+
* @param {number|string} count - Count to sanitize
|
|
227
|
+
* @returns {number} Sanitized count (0 if invalid)
|
|
228
|
+
*/
|
|
229
|
+
count(count) {
|
|
230
|
+
const num = typeof count === 'string' ? parseInt(count, 10) : count;
|
|
231
|
+
if (!Number.isFinite(num) || num < 0) return 0;
|
|
232
|
+
return Math.floor(num);
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Sanitize a version string
|
|
237
|
+
* @param {string} version - Version to sanitize
|
|
238
|
+
* @returns {string} Sanitized version or 'unknown'
|
|
239
|
+
*/
|
|
240
|
+
version(version) {
|
|
241
|
+
if (!version || typeof version !== 'string') return 'unknown';
|
|
242
|
+
|
|
243
|
+
const cleaned = removeControlChars(version).trim();
|
|
244
|
+
if (!PATTERNS.version.test(cleaned)) {
|
|
245
|
+
return 'unknown';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return truncate(cleaned, MAX_LENGTHS.version);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Sanitize a date string (ISO format)
|
|
253
|
+
* @param {string|Date} date - Date to sanitize
|
|
254
|
+
* @returns {string} Sanitized date in YYYY-MM-DD format
|
|
255
|
+
*/
|
|
256
|
+
date(date) {
|
|
257
|
+
if (date instanceof Date) {
|
|
258
|
+
return date.toISOString().split('T')[0];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!date || typeof date !== 'string') {
|
|
262
|
+
return new Date().toISOString().split('T')[0];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const cleaned = removeControlChars(date).trim();
|
|
266
|
+
if (!PATTERNS.date.test(cleaned)) {
|
|
267
|
+
return new Date().toISOString().split('T')[0];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return cleaned;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sanitize a folder name
|
|
275
|
+
* @param {string} name - Folder name to sanitize
|
|
276
|
+
* @param {string} [defaultName='.agileflow'] - Default if invalid
|
|
277
|
+
* @returns {string} Sanitized folder name
|
|
278
|
+
*/
|
|
279
|
+
folderName(name, defaultName = '.agileflow') {
|
|
280
|
+
if (!name || typeof name !== 'string') return defaultName;
|
|
281
|
+
|
|
282
|
+
const cleaned = removeControlChars(name).trim();
|
|
283
|
+
if (!PATTERNS.folderName.test(cleaned)) {
|
|
284
|
+
return defaultName;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return truncate(cleaned, MAX_LENGTHS.folderName);
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Sanitize a tool name for agent tools list
|
|
292
|
+
* @param {string} tool - Tool name to sanitize
|
|
293
|
+
* @returns {string} Sanitized tool name
|
|
294
|
+
*/
|
|
295
|
+
toolName(tool) {
|
|
296
|
+
if (!tool || typeof tool !== 'string') return '';
|
|
297
|
+
|
|
298
|
+
const cleaned = removeControlChars(tool).trim();
|
|
299
|
+
if (!PATTERNS.toolName.test(cleaned)) {
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return truncate(cleaned, MAX_LENGTHS.toolName);
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sanitize a model name
|
|
308
|
+
* @param {string} model - Model name to sanitize
|
|
309
|
+
* @param {string} [defaultModel='haiku'] - Default if invalid
|
|
310
|
+
* @returns {string} Sanitized model name
|
|
311
|
+
*/
|
|
312
|
+
modelName(model, defaultModel = 'haiku') {
|
|
313
|
+
if (!model || typeof model !== 'string') return defaultModel;
|
|
314
|
+
|
|
315
|
+
const cleaned = removeControlChars(model).trim().toLowerCase();
|
|
316
|
+
if (!PATTERNS.modelName.test(cleaned)) {
|
|
317
|
+
return defaultModel;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return truncate(cleaned, MAX_LENGTHS.modelName);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Sanitize an array of tool names
|
|
325
|
+
* @param {string[]} tools - Tool names to sanitize
|
|
326
|
+
* @returns {string[]} Sanitized tool names (empty entries removed)
|
|
327
|
+
*/
|
|
328
|
+
toolsList(tools) {
|
|
329
|
+
if (!Array.isArray(tools)) return [];
|
|
330
|
+
|
|
331
|
+
return tools.map(t => sanitize.toolName(t)).filter(Boolean);
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Validate a placeholder value against expected type
|
|
337
|
+
* @param {string} placeholder - Placeholder name (e.g., 'COMMAND_COUNT')
|
|
338
|
+
* @param {any} value - Value to validate
|
|
339
|
+
* @returns {{ valid: boolean, sanitized?: any, error?: string }}
|
|
340
|
+
*/
|
|
341
|
+
function validatePlaceholderValue(placeholder, value) {
|
|
342
|
+
switch (placeholder) {
|
|
343
|
+
case 'COMMAND_COUNT':
|
|
344
|
+
case 'AGENT_COUNT':
|
|
345
|
+
case 'SKILL_COUNT': {
|
|
346
|
+
const sanitized = sanitize.count(value);
|
|
347
|
+
return { valid: true, sanitized };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case 'VERSION': {
|
|
351
|
+
const sanitized = sanitize.version(value);
|
|
352
|
+
if (sanitized === 'unknown' && value !== 'unknown') {
|
|
353
|
+
return { valid: false, error: `Invalid version format: ${value}` };
|
|
354
|
+
}
|
|
355
|
+
return { valid: true, sanitized };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
case 'INSTALL_DATE': {
|
|
359
|
+
const sanitized = sanitize.date(value);
|
|
360
|
+
return { valid: true, sanitized };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case 'agileflow_folder': {
|
|
364
|
+
const sanitized = sanitize.folderName(value);
|
|
365
|
+
if (sanitized !== value) {
|
|
366
|
+
return { valid: false, error: `Invalid folder name: ${value}` };
|
|
367
|
+
}
|
|
368
|
+
return { valid: true, sanitized };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
default:
|
|
372
|
+
return { valid: true, sanitized: value };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Sanitize agent data for list generation
|
|
378
|
+
* @param {Object} agent - Agent data from frontmatter
|
|
379
|
+
* @returns {Object} Sanitized agent data
|
|
380
|
+
*/
|
|
381
|
+
function sanitizeAgentData(agent) {
|
|
382
|
+
return {
|
|
383
|
+
name: sanitize.name(agent.name, { strict: false }) || 'unknown',
|
|
384
|
+
description: sanitize.description(agent.description || ''),
|
|
385
|
+
tools: sanitize.toolsList(agent.tools || []),
|
|
386
|
+
model: sanitize.modelName(agent.model),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Sanitize command data for list generation
|
|
392
|
+
* @param {Object} command - Command data from frontmatter
|
|
393
|
+
* @returns {Object} Sanitized command data
|
|
394
|
+
*/
|
|
395
|
+
function sanitizeCommandData(command) {
|
|
396
|
+
return {
|
|
397
|
+
name: sanitize.name(command.name, { strict: false }) || 'unknown',
|
|
398
|
+
description: sanitize.description(command.description || ''),
|
|
399
|
+
argumentHint: sanitize.description(command.argumentHint || '', { escapeMarkdown: true }),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if content appears to contain injection attempts
|
|
405
|
+
* @param {string} content - Content to check
|
|
406
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
407
|
+
*/
|
|
408
|
+
function detectInjectionAttempt(content) {
|
|
409
|
+
if (!content || typeof content !== 'string') {
|
|
410
|
+
return { safe: true };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check for shell injection patterns
|
|
414
|
+
const shellInjectionPatterns = [
|
|
415
|
+
/\$\(/, // Command substitution
|
|
416
|
+
/`[^`]+`/, // Backtick execution
|
|
417
|
+
/;\s*rm\s/i, // rm command
|
|
418
|
+
/;\s*dd\s/i, // dd command
|
|
419
|
+
/\|\s*sh\b/i, // Piping to shell
|
|
420
|
+
/>\s*\/etc\//i, // Writing to /etc
|
|
421
|
+
/\/dev\/null/i, // /dev/null (suspicious in content)
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
for (const pattern of shellInjectionPatterns) {
|
|
425
|
+
if (pattern.test(content)) {
|
|
426
|
+
return { safe: false, reason: `Suspicious pattern detected: ${pattern}` };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check for markdown injection that could break document structure
|
|
431
|
+
const markdownInjectionPatterns = [
|
|
432
|
+
/^#+ /m, // Unexpected headers (could break structure)
|
|
433
|
+
/\[.*\]\(javascript:/i, // JavaScript URLs
|
|
434
|
+
/\[.*\]\(data:/i, // Data URLs
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
for (const pattern of markdownInjectionPatterns) {
|
|
438
|
+
if (pattern.test(content)) {
|
|
439
|
+
return { safe: false, reason: `Markdown injection pattern detected: ${pattern}` };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { safe: true };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
module.exports = {
|
|
447
|
+
// Patterns and constants
|
|
448
|
+
PATTERNS,
|
|
449
|
+
MAX_LENGTHS,
|
|
450
|
+
|
|
451
|
+
// Core sanitization functions
|
|
452
|
+
sanitize,
|
|
453
|
+
escapeMarkdown,
|
|
454
|
+
escapeShell,
|
|
455
|
+
removeControlChars,
|
|
456
|
+
truncate,
|
|
457
|
+
|
|
458
|
+
// Validation
|
|
459
|
+
validatePlaceholderValue,
|
|
460
|
+
sanitizeAgentData,
|
|
461
|
+
sanitizeCommandData,
|
|
462
|
+
detectInjectionAttempt,
|
|
463
|
+
};
|