agileflow 2.87.0 → 2.89.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/lib/file-cache.js +358 -0
- package/lib/progress.js +331 -0
- package/lib/validate.js +281 -1
- package/lib/yaml-utils.js +122 -0
- package/package.json +1 -3
- package/scripts/agileflow-welcome.js +19 -33
- package/scripts/damage-control-bash.js +2 -59
- package/scripts/damage-control-edit.js +4 -69
- package/scripts/damage-control-write.js +4 -69
- package/scripts/lib/damage-control-utils.js +153 -0
- package/scripts/obtain-context.js +4 -5
- package/tools/cli/installers/core/installer.js +32 -2
- package/tools/cli/installers/ide/_base-ide.js +143 -19
- package/tools/cli/installers/ide/claude-code.js +14 -51
- package/tools/cli/installers/ide/cursor.js +4 -40
- package/tools/cli/installers/ide/windsurf.js +6 -40
- package/tools/cli/lib/content-injector.js +37 -0
- package/tools/cli/lib/ide-errors.js +233 -0
package/lib/validate.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* AgileFlow CLI - Input Validation Utilities
|
|
3
3
|
*
|
|
4
4
|
* Centralized validation patterns and helpers to prevent
|
|
5
|
-
* command injection and invalid input handling.
|
|
5
|
+
* command injection, path traversal, and invalid input handling.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Validation patterns for common input types.
|
|
10
13
|
* All patterns use strict whitelisting approach.
|
|
@@ -320,7 +323,277 @@ function validateArgs(args, schema) {
|
|
|
320
323
|
return { ok: true, data: result };
|
|
321
324
|
}
|
|
322
325
|
|
|
326
|
+
// =============================================================================
|
|
327
|
+
// Path Traversal Protection
|
|
328
|
+
// =============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Path validation error with context.
|
|
332
|
+
*/
|
|
333
|
+
class PathValidationError extends Error {
|
|
334
|
+
/**
|
|
335
|
+
* @param {string} message - Error message
|
|
336
|
+
* @param {string} inputPath - The problematic path
|
|
337
|
+
* @param {string} reason - Reason for rejection
|
|
338
|
+
*/
|
|
339
|
+
constructor(message, inputPath, reason) {
|
|
340
|
+
super(message);
|
|
341
|
+
this.name = 'PathValidationError';
|
|
342
|
+
this.inputPath = inputPath;
|
|
343
|
+
this.reason = reason;
|
|
344
|
+
Error.captureStackTrace(this, this.constructor);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Validate that a path is safe and within the allowed base directory.
|
|
350
|
+
* Prevents path traversal attacks by:
|
|
351
|
+
* 1. Resolving the path to absolute form
|
|
352
|
+
* 2. Ensuring it stays within the base directory
|
|
353
|
+
* 3. Rejecting symbolic links (optional)
|
|
354
|
+
*
|
|
355
|
+
* @param {string} inputPath - The path to validate (can be relative or absolute)
|
|
356
|
+
* @param {string} baseDir - The allowed base directory (must be absolute)
|
|
357
|
+
* @param {Object} options - Validation options
|
|
358
|
+
* @param {boolean} [options.allowSymlinks=false] - Allow symbolic links
|
|
359
|
+
* @param {boolean} [options.mustExist=false] - Path must exist on filesystem
|
|
360
|
+
* @returns {{ ok: boolean, resolvedPath?: string, error?: PathValidationError }}
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* // Validate a file path within project directory
|
|
364
|
+
* const result = validatePath('./config.yaml', '/home/user/project');
|
|
365
|
+
* if (result.ok) {
|
|
366
|
+
* console.log('Safe path:', result.resolvedPath);
|
|
367
|
+
* }
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* // Reject path traversal attempt
|
|
371
|
+
* const result = validatePath('../../../etc/passwd', '/home/user/project');
|
|
372
|
+
* // result.ok === false
|
|
373
|
+
* // result.error.reason === 'path_traversal'
|
|
374
|
+
*/
|
|
375
|
+
function validatePath(inputPath, baseDir, options = {}) {
|
|
376
|
+
const { allowSymlinks = false, mustExist = false } = options;
|
|
377
|
+
|
|
378
|
+
// Input validation
|
|
379
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
error: new PathValidationError(
|
|
383
|
+
'Path is required and must be a string',
|
|
384
|
+
String(inputPath),
|
|
385
|
+
'invalid_input'
|
|
386
|
+
),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!baseDir || typeof baseDir !== 'string') {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
error: new PathValidationError(
|
|
394
|
+
'Base directory is required and must be a string',
|
|
395
|
+
inputPath,
|
|
396
|
+
'invalid_base'
|
|
397
|
+
),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Base directory must be absolute
|
|
402
|
+
if (!path.isAbsolute(baseDir)) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: new PathValidationError(
|
|
406
|
+
'Base directory must be an absolute path',
|
|
407
|
+
inputPath,
|
|
408
|
+
'relative_base'
|
|
409
|
+
),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Normalize the base directory
|
|
414
|
+
const normalizedBase = path.resolve(baseDir);
|
|
415
|
+
|
|
416
|
+
// Resolve the input path relative to base directory
|
|
417
|
+
let resolvedPath;
|
|
418
|
+
if (path.isAbsolute(inputPath)) {
|
|
419
|
+
resolvedPath = path.resolve(inputPath);
|
|
420
|
+
} else {
|
|
421
|
+
resolvedPath = path.resolve(normalizedBase, inputPath);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check for path traversal: resolved path must start with base directory
|
|
425
|
+
// Add trailing separator to prevent prefix attacks (e.g., /home/user vs /home/user2)
|
|
426
|
+
const baseWithSep = normalizedBase.endsWith(path.sep)
|
|
427
|
+
? normalizedBase
|
|
428
|
+
: normalizedBase + path.sep;
|
|
429
|
+
|
|
430
|
+
const isWithinBase =
|
|
431
|
+
resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
|
|
432
|
+
|
|
433
|
+
if (!isWithinBase) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
error: new PathValidationError(
|
|
437
|
+
`Path escapes base directory: ${inputPath}`,
|
|
438
|
+
inputPath,
|
|
439
|
+
'path_traversal'
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if path exists (if required)
|
|
445
|
+
if (mustExist) {
|
|
446
|
+
try {
|
|
447
|
+
fs.accessSync(resolvedPath);
|
|
448
|
+
} catch {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
error: new PathValidationError(
|
|
452
|
+
`Path does not exist: ${resolvedPath}`,
|
|
453
|
+
inputPath,
|
|
454
|
+
'not_found'
|
|
455
|
+
),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check for symbolic links (if not allowed)
|
|
461
|
+
if (!allowSymlinks) {
|
|
462
|
+
try {
|
|
463
|
+
const stats = fs.lstatSync(resolvedPath);
|
|
464
|
+
if (stats.isSymbolicLink()) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
error: new PathValidationError(
|
|
468
|
+
`Symbolic links are not allowed: ${inputPath}`,
|
|
469
|
+
inputPath,
|
|
470
|
+
'symlink_rejected'
|
|
471
|
+
),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
// Path doesn't exist yet, which is fine if mustExist is false
|
|
476
|
+
// Check parent directories for symlinks
|
|
477
|
+
const parts = path.relative(normalizedBase, resolvedPath).split(path.sep);
|
|
478
|
+
let currentPath = normalizedBase;
|
|
479
|
+
|
|
480
|
+
for (const part of parts) {
|
|
481
|
+
currentPath = path.join(currentPath, part);
|
|
482
|
+
try {
|
|
483
|
+
const stats = fs.lstatSync(currentPath);
|
|
484
|
+
if (stats.isSymbolicLink()) {
|
|
485
|
+
return {
|
|
486
|
+
ok: false,
|
|
487
|
+
error: new PathValidationError(
|
|
488
|
+
`Path contains symbolic link: ${currentPath}`,
|
|
489
|
+
inputPath,
|
|
490
|
+
'symlink_in_path'
|
|
491
|
+
),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
// Part of path doesn't exist, stop checking
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
resolvedPath,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Synchronous version that throws on invalid paths.
|
|
510
|
+
* Use when you want exceptions rather than result objects.
|
|
511
|
+
*
|
|
512
|
+
* @param {string} inputPath - The path to validate
|
|
513
|
+
* @param {string} baseDir - The allowed base directory
|
|
514
|
+
* @param {Object} options - Validation options
|
|
515
|
+
* @returns {string} The validated absolute path
|
|
516
|
+
* @throws {PathValidationError} If path is invalid
|
|
517
|
+
*/
|
|
518
|
+
function validatePathSync(inputPath, baseDir, options = {}) {
|
|
519
|
+
const result = validatePath(inputPath, baseDir, options);
|
|
520
|
+
if (!result.ok) {
|
|
521
|
+
throw result.error;
|
|
522
|
+
}
|
|
523
|
+
return result.resolvedPath;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Check if a path contains dangerous patterns without resolving.
|
|
528
|
+
* Useful for quick pre-validation before expensive operations.
|
|
529
|
+
*
|
|
530
|
+
* @param {string} inputPath - The path to check
|
|
531
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
532
|
+
*/
|
|
533
|
+
function hasUnsafePathPatterns(inputPath) {
|
|
534
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
535
|
+
return { safe: false, reason: 'invalid_input' };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Check for null bytes (can bypass security in some systems)
|
|
539
|
+
if (inputPath.includes('\0')) {
|
|
540
|
+
return { safe: false, reason: 'null_byte' };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Check for obvious traversal patterns
|
|
544
|
+
if (inputPath.includes('..')) {
|
|
545
|
+
return { safe: false, reason: 'dot_dot_sequence' };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Check for absolute paths on Unix when expecting relative
|
|
549
|
+
if (inputPath.startsWith('/') && !path.isAbsolute(inputPath)) {
|
|
550
|
+
return { safe: false, reason: 'unexpected_absolute' };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check for Windows-style absolute paths
|
|
554
|
+
if (/^[a-zA-Z]:/.test(inputPath)) {
|
|
555
|
+
return { safe: false, reason: 'windows_absolute' };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { safe: true };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Sanitize a filename by removing dangerous characters.
|
|
563
|
+
* Does NOT validate the full path - use with validatePath().
|
|
564
|
+
*
|
|
565
|
+
* @param {string} filename - The filename to sanitize
|
|
566
|
+
* @param {Object} options - Sanitization options
|
|
567
|
+
* @param {string} [options.replacement='_'] - Character to replace with
|
|
568
|
+
* @param {number} [options.maxLength=255] - Maximum filename length
|
|
569
|
+
* @returns {string} Sanitized filename
|
|
570
|
+
*/
|
|
571
|
+
function sanitizeFilename(filename, options = {}) {
|
|
572
|
+
const { replacement = '_', maxLength = 255 } = options;
|
|
573
|
+
|
|
574
|
+
if (!filename || typeof filename !== 'string') {
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Remove or replace dangerous characters
|
|
579
|
+
let sanitized = filename
|
|
580
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, replacement) // Control chars and reserved
|
|
581
|
+
.replace(/\.{2,}/g, replacement) // Multiple dots
|
|
582
|
+
.replace(/^\.+/, replacement) // Leading dots
|
|
583
|
+
.replace(/^-+/, replacement); // Leading dashes (prevent flag injection)
|
|
584
|
+
|
|
585
|
+
// Truncate if too long
|
|
586
|
+
if (sanitized.length > maxLength) {
|
|
587
|
+
const ext = path.extname(sanitized);
|
|
588
|
+
const base = path.basename(sanitized, ext);
|
|
589
|
+
sanitized = base.slice(0, maxLength - ext.length) + ext;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return sanitized;
|
|
593
|
+
}
|
|
594
|
+
|
|
323
595
|
module.exports = {
|
|
596
|
+
// Patterns and basic validators
|
|
324
597
|
PATTERNS,
|
|
325
598
|
isValidBranchName,
|
|
326
599
|
isValidStoryId,
|
|
@@ -334,4 +607,11 @@ module.exports = {
|
|
|
334
607
|
parseIntBounded,
|
|
335
608
|
isValidOption,
|
|
336
609
|
validateArgs,
|
|
610
|
+
|
|
611
|
+
// Path traversal protection
|
|
612
|
+
PathValidationError,
|
|
613
|
+
validatePath,
|
|
614
|
+
validatePathSync,
|
|
615
|
+
hasUnsafePathPatterns,
|
|
616
|
+
sanitizeFilename,
|
|
337
617
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized YAML parsing utilities with security guarantees.
|
|
3
|
+
*
|
|
4
|
+
* Security Note (js-yaml v4.x):
|
|
5
|
+
* - yaml.load() is SAFE by default in v4+ (unlike v3.x)
|
|
6
|
+
* - Dangerous JavaScript types (functions, regexps) are NOT supported
|
|
7
|
+
* - Only JSON-compatible types are parsed
|
|
8
|
+
* - See: https://github.com/nodeca/js-yaml/blob/master/migrate_v3_to_v4.md
|
|
9
|
+
*
|
|
10
|
+
* This wrapper provides:
|
|
11
|
+
* - Explicit security documentation
|
|
12
|
+
* - Input validation
|
|
13
|
+
* - Consistent error handling
|
|
14
|
+
* - Future-proofing for library changes
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Safely parse YAML content. Uses js-yaml's DEFAULT_SCHEMA which is
|
|
21
|
+
* safe by default in v4+ (does not execute arbitrary JavaScript).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} content - YAML string to parse
|
|
24
|
+
* @param {Object} options - Optional yaml.load options
|
|
25
|
+
* @returns {any} Parsed YAML content (object, array, or primitive)
|
|
26
|
+
* @throws {yaml.YAMLException} If YAML is malformed
|
|
27
|
+
*/
|
|
28
|
+
function safeLoad(content, options = {}) {
|
|
29
|
+
if (typeof content !== 'string') {
|
|
30
|
+
throw new TypeError('YAML content must be a string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// In js-yaml v4+, load() uses DEFAULT_SCHEMA which is safe.
|
|
34
|
+
// Explicitly pass schema to make the security guarantee clear
|
|
35
|
+
// and protect against future library changes.
|
|
36
|
+
return yaml.load(content, {
|
|
37
|
+
schema: yaml.DEFAULT_SCHEMA,
|
|
38
|
+
...options,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Safely parse all YAML documents in a multi-document file.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} content - YAML string containing one or more documents
|
|
46
|
+
* @param {Object} options - Optional yaml.loadAll options
|
|
47
|
+
* @returns {any[]} Array of parsed YAML documents
|
|
48
|
+
* @throws {yaml.YAMLException} If YAML is malformed
|
|
49
|
+
*/
|
|
50
|
+
function safeLoadAll(content, options = {}) {
|
|
51
|
+
if (typeof content !== 'string') {
|
|
52
|
+
throw new TypeError('YAML content must be a string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const documents = [];
|
|
56
|
+
yaml.loadAll(
|
|
57
|
+
content,
|
|
58
|
+
(doc) => documents.push(doc),
|
|
59
|
+
{ schema: yaml.DEFAULT_SCHEMA, ...options }
|
|
60
|
+
);
|
|
61
|
+
return documents;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Safely serialize data to YAML string.
|
|
66
|
+
*
|
|
67
|
+
* @param {any} data - Data to serialize
|
|
68
|
+
* @param {Object} options - Optional yaml.dump options
|
|
69
|
+
* @returns {string} YAML string representation
|
|
70
|
+
*/
|
|
71
|
+
function safeDump(data, options = {}) {
|
|
72
|
+
return yaml.dump(data, {
|
|
73
|
+
schema: yaml.DEFAULT_SCHEMA,
|
|
74
|
+
...options,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Test if js-yaml is configured securely (no JavaScript type support).
|
|
80
|
+
* This function is used in security tests to verify the library version
|
|
81
|
+
* and configuration.
|
|
82
|
+
*
|
|
83
|
+
* @returns {boolean} True if the configuration is secure
|
|
84
|
+
*/
|
|
85
|
+
function isSecureConfiguration() {
|
|
86
|
+
// Attempt to parse YAML with JavaScript function syntax
|
|
87
|
+
// This should NOT execute any code in v4+
|
|
88
|
+
const maliciousYaml = `
|
|
89
|
+
test: !!js/function 'function() { return "pwned"; }'
|
|
90
|
+
regex: !!js/regexp /test/i
|
|
91
|
+
undef: !!js/undefined ''
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = safeLoad(maliciousYaml);
|
|
96
|
+
// If we get here without executing code, and the values are
|
|
97
|
+
// either strings or undefined (not actual functions), we're safe
|
|
98
|
+
if (typeof result.test === 'function') {
|
|
99
|
+
return false; // Unsafe: function was instantiated
|
|
100
|
+
}
|
|
101
|
+
if (result.regex instanceof RegExp) {
|
|
102
|
+
return false; // Unsafe: regex was instantiated
|
|
103
|
+
}
|
|
104
|
+
// Values should be undefined or error out due to unknown tags
|
|
105
|
+
return true;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// YAMLException for unknown tags is the expected safe behavior
|
|
108
|
+
if (e.name === 'YAMLException') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
safeLoad,
|
|
117
|
+
safeLoadAll,
|
|
118
|
+
safeDump,
|
|
119
|
+
isSecureConfiguration,
|
|
120
|
+
// Re-export yaml module for edge cases that need direct access
|
|
121
|
+
yaml,
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agileflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.89.0",
|
|
4
4
|
"description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agile",
|
|
@@ -53,8 +53,6 @@
|
|
|
53
53
|
"test:coverage": "jest --coverage"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"blessed": "^0.1.81",
|
|
57
|
-
"blessed-contrib": "^4.10.1",
|
|
58
56
|
"chalk": "^4.1.2",
|
|
59
57
|
"commander": "^12.1.0",
|
|
60
58
|
"fs-extra": "^11.2.0",
|
|
@@ -18,6 +18,7 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
18
18
|
// Shared utilities
|
|
19
19
|
const { c, box } = require('../lib/colors');
|
|
20
20
|
const { getProjectRoot } = require('../lib/paths');
|
|
21
|
+
const { readJSONCached, readFileCached } = require('../lib/file-cache');
|
|
21
22
|
|
|
22
23
|
// Session manager path (relative to script location)
|
|
23
24
|
const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
|
|
@@ -47,44 +48,29 @@ try {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* PERFORMANCE OPTIMIZATION: Load all project files
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* PERFORMANCE OPTIMIZATION: Load all project files using LRU cache
|
|
52
|
+
* Uses file-cache module for automatic caching with 30s TTL.
|
|
53
|
+
* Files are cached across script invocations within TTL window.
|
|
54
|
+
* Estimated savings: 60-120ms on cache hits
|
|
53
55
|
*/
|
|
54
56
|
function loadProjectFiles(rootDir) {
|
|
55
|
-
const cache = {
|
|
56
|
-
status: null,
|
|
57
|
-
metadata: null,
|
|
58
|
-
settings: null,
|
|
59
|
-
sessionState: null,
|
|
60
|
-
configYaml: null,
|
|
61
|
-
cliPackage: null,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
57
|
const paths = {
|
|
65
|
-
status: 'docs
|
|
66
|
-
metadata: 'docs
|
|
67
|
-
settings: '.claude
|
|
68
|
-
sessionState: 'docs
|
|
69
|
-
configYaml: '.agileflow
|
|
70
|
-
cliPackage: 'packages
|
|
58
|
+
status: path.join(rootDir, 'docs', '09-agents', 'status.json'),
|
|
59
|
+
metadata: path.join(rootDir, 'docs', '00-meta', 'agileflow-metadata.json'),
|
|
60
|
+
settings: path.join(rootDir, '.claude', 'settings.json'),
|
|
61
|
+
sessionState: path.join(rootDir, 'docs', '09-agents', 'session-state.json'),
|
|
62
|
+
configYaml: path.join(rootDir, '.agileflow', 'config.yaml'),
|
|
63
|
+
cliPackage: path.join(rootDir, 'packages', 'cli', 'package.json'),
|
|
71
64
|
};
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
} catch (e) {
|
|
83
|
-
// Silently ignore - file not available or invalid
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return cache;
|
|
66
|
+
return {
|
|
67
|
+
status: readJSONCached(paths.status),
|
|
68
|
+
metadata: readJSONCached(paths.metadata),
|
|
69
|
+
settings: readJSONCached(paths.settings),
|
|
70
|
+
sessionState: readJSONCached(paths.sessionState),
|
|
71
|
+
configYaml: readFileCached(paths.configYaml),
|
|
72
|
+
cliPackage: readJSONCached(paths.cliPackage),
|
|
73
|
+
};
|
|
88
74
|
}
|
|
89
75
|
|
|
90
76
|
/**
|
|
@@ -20,67 +20,10 @@ const {
|
|
|
20
20
|
loadPatterns,
|
|
21
21
|
outputBlocked,
|
|
22
22
|
runDamageControlHook,
|
|
23
|
+
parseBashPatterns,
|
|
23
24
|
c,
|
|
24
25
|
} = require('./lib/damage-control-utils');
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Parse simplified YAML for damage control patterns
|
|
28
|
-
* Only parses the structure we need - not a full YAML parser
|
|
29
|
-
*/
|
|
30
|
-
function parseSimpleYAML(content) {
|
|
31
|
-
const config = {
|
|
32
|
-
bashToolPatterns: [],
|
|
33
|
-
askPatterns: [],
|
|
34
|
-
agileflowProtections: [],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
let currentSection = null;
|
|
38
|
-
let currentPattern = null;
|
|
39
|
-
|
|
40
|
-
for (const line of content.split('\n')) {
|
|
41
|
-
const trimmed = line.trim();
|
|
42
|
-
|
|
43
|
-
// Skip empty lines and comments
|
|
44
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
45
|
-
|
|
46
|
-
// Detect section headers
|
|
47
|
-
if (trimmed === 'bashToolPatterns:') {
|
|
48
|
-
currentSection = 'bashToolPatterns';
|
|
49
|
-
currentPattern = null;
|
|
50
|
-
} else if (trimmed === 'askPatterns:') {
|
|
51
|
-
currentSection = 'askPatterns';
|
|
52
|
-
currentPattern = null;
|
|
53
|
-
} else if (trimmed === 'agileflowProtections:') {
|
|
54
|
-
currentSection = 'agileflowProtections';
|
|
55
|
-
currentPattern = null;
|
|
56
|
-
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
57
|
-
// Other sections we don't care about for bash validation
|
|
58
|
-
currentSection = null;
|
|
59
|
-
currentPattern = null;
|
|
60
|
-
} else if (trimmed.startsWith('- pattern:') && currentSection) {
|
|
61
|
-
// New pattern entry
|
|
62
|
-
const patternValue = trimmed
|
|
63
|
-
.replace('- pattern:', '')
|
|
64
|
-
.trim()
|
|
65
|
-
.replace(/^["']|["']$/g, '');
|
|
66
|
-
currentPattern = { pattern: patternValue };
|
|
67
|
-
config[currentSection].push(currentPattern);
|
|
68
|
-
} else if (trimmed.startsWith('reason:') && currentPattern) {
|
|
69
|
-
currentPattern.reason = trimmed
|
|
70
|
-
.replace('reason:', '')
|
|
71
|
-
.trim()
|
|
72
|
-
.replace(/^["']|["']$/g, '');
|
|
73
|
-
} else if (trimmed.startsWith('flags:') && currentPattern) {
|
|
74
|
-
currentPattern.flags = trimmed
|
|
75
|
-
.replace('flags:', '')
|
|
76
|
-
.trim()
|
|
77
|
-
.replace(/^["']|["']$/g, '');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return config;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
27
|
/**
|
|
85
28
|
* Test command against a single pattern rule
|
|
86
29
|
*/
|
|
@@ -134,7 +77,7 @@ const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtecti
|
|
|
134
77
|
|
|
135
78
|
runDamageControlHook({
|
|
136
79
|
getInputValue: input => input.command || input.tool_input?.command,
|
|
137
|
-
loadConfig: () => loadPatterns(projectRoot,
|
|
80
|
+
loadConfig: () => loadPatterns(projectRoot, parseBashPatterns, defaultConfig),
|
|
138
81
|
validate: validateCommand,
|
|
139
82
|
onBlock: (result, command) => {
|
|
140
83
|
outputBlocked(
|
|
@@ -15,85 +15,20 @@
|
|
|
15
15
|
const {
|
|
16
16
|
findProjectRoot,
|
|
17
17
|
loadPatterns,
|
|
18
|
-
pathMatches,
|
|
19
18
|
outputBlocked,
|
|
20
19
|
runDamageControlHook,
|
|
20
|
+
parsePathPatterns,
|
|
21
|
+
validatePathAgainstPatterns,
|
|
21
22
|
} = require('./lib/damage-control-utils');
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Parse simplified YAML for path patterns
|
|
25
|
-
*/
|
|
26
|
-
function parseSimpleYAML(content) {
|
|
27
|
-
const config = {
|
|
28
|
-
zeroAccessPaths: [],
|
|
29
|
-
readOnlyPaths: [],
|
|
30
|
-
noDeletePaths: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let currentSection = null;
|
|
34
|
-
|
|
35
|
-
for (const line of content.split('\n')) {
|
|
36
|
-
const trimmed = line.trim();
|
|
37
|
-
|
|
38
|
-
// Skip empty lines and comments
|
|
39
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
40
|
-
|
|
41
|
-
// Detect section headers
|
|
42
|
-
if (trimmed === 'zeroAccessPaths:') {
|
|
43
|
-
currentSection = 'zeroAccessPaths';
|
|
44
|
-
} else if (trimmed === 'readOnlyPaths:') {
|
|
45
|
-
currentSection = 'readOnlyPaths';
|
|
46
|
-
} else if (trimmed === 'noDeletePaths:') {
|
|
47
|
-
currentSection = 'noDeletePaths';
|
|
48
|
-
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
49
|
-
// Other sections we don't care about for path validation
|
|
50
|
-
currentSection = null;
|
|
51
|
-
} else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
|
|
52
|
-
// Path entry
|
|
53
|
-
const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
54
|
-
config[currentSection].push(pathValue);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return config;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate file path for edit operation
|
|
63
|
-
*/
|
|
64
|
-
function validatePath(filePath, config) {
|
|
65
|
-
// Check zero access paths - completely blocked
|
|
66
|
-
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
67
|
-
if (zeroMatch) {
|
|
68
|
-
return {
|
|
69
|
-
action: 'block',
|
|
70
|
-
reason: `Zero-access path: ${zeroMatch}`,
|
|
71
|
-
detail: 'This file is protected and cannot be accessed',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check read-only paths - cannot edit
|
|
76
|
-
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
77
|
-
if (readOnlyMatch) {
|
|
78
|
-
return {
|
|
79
|
-
action: 'block',
|
|
80
|
-
reason: `Read-only path: ${readOnlyMatch}`,
|
|
81
|
-
detail: 'This file is read-only and cannot be edited',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Allow by default
|
|
86
|
-
return { action: 'allow' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
24
|
// Run the hook
|
|
90
25
|
const projectRoot = findProjectRoot();
|
|
91
26
|
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
92
27
|
|
|
93
28
|
runDamageControlHook({
|
|
94
29
|
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
95
|
-
loadConfig: () => loadPatterns(projectRoot,
|
|
96
|
-
validate:
|
|
30
|
+
loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
|
|
31
|
+
validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'edit'),
|
|
97
32
|
onBlock: (result, filePath) => {
|
|
98
33
|
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
99
34
|
},
|