@lsendel/claude-agents
Version:
Supercharge Claude Code with specialized AI sub-agents for code review, testing, debugging, documentation & more. Now with process & standards management! Easy CLI tool to install, manage & create custom AI agents for enhanced development workflow
245 lines (208 loc) • 6.26 kB
JavaScript
import path from 'path';
// Regex patterns for validation
const AGENT_NAME_PATTERN = /^[a-z0-9-]+$/;
const COMMAND_INJECTION_PATTERNS = [/[;&|`$(){}[\]<>]/, /\.\./, /~\//];
/**
* Validates agent name format
* @param {string} agentName - The agent name to validate
* @returns {import('../../types').ValidationResult} - Validation result
*/
export function validateAgentName(agentName) {
if (!agentName || typeof agentName !== 'string') {
return { valid: false, error: 'Agent name must be a non-empty string' };
}
if (agentName.length < 3 || agentName.length > 50) {
return {
valid: false,
error: 'Agent name must be between 3 and 50 characters',
};
}
if (!AGENT_NAME_PATTERN.test(agentName)) {
return {
valid: false,
error:
'Agent name can only contain lowercase letters, numbers, and hyphens',
};
}
if (agentName.startsWith('-') || agentName.endsWith('-')) {
return {
valid: false,
error: 'Agent name cannot start or end with a hyphen',
};
}
// Prevent path traversal attacks
if (
agentName.includes('..') ||
agentName.includes('/') ||
agentName.includes('\\')
) {
return { valid: false, error: 'Agent name contains invalid characters' };
}
// Block common path traversal patterns
const dangerousPatterns = [
'%2e',
'%2f',
'%5c',
'%252e',
'%252f',
'%255c',
'~',
];
for (const pattern of dangerousPatterns) {
if (agentName.toLowerCase().includes(pattern)) {
return {
valid: false,
error: 'Agent name contains potentially dangerous patterns',
};
}
}
return { valid: true };
}
/**
* Validates file path for safety
* @param {string} filePath - The file path to validate
* @returns {Object} - { valid: boolean, error?: string }
*/
export function validateFilePath(filePath) {
if (!filePath || typeof filePath !== 'string') {
return { valid: false, error: 'File path must be a non-empty string' };
}
// Check for command injection attempts
for (const pattern of COMMAND_INJECTION_PATTERNS) {
if (pattern.test(filePath)) {
return { valid: false, error: 'File path contains invalid characters' };
}
}
// Ensure path doesn't escape project boundaries
const normalizedPath = path.normalize(filePath);
const resolvedPath = path.resolve(filePath);
if (normalizedPath !== filePath || !resolvedPath.startsWith(process.cwd())) {
return {
valid: false,
error: 'File path attempts to access restricted directories',
};
}
return { valid: true };
}
/**
* Sanitizes user input to prevent injection attacks
* @param {string} input - The input to sanitize
* @returns {string} - Sanitized input
*/
export function sanitizeInput(input) {
if (!input || typeof input !== 'string') {
return '';
}
// Remove null bytes
let sanitized = input.replace(/\0/g, '');
// Escape shell metacharacters
sanitized = sanitized.replace(/([`$\\])/g, '\\$1');
// Limit length to prevent DoS
if (sanitized.length > 1000) {
sanitized = sanitized.substring(0, 1000);
}
return sanitized;
}
/**
* Validates command options
* @param {Object} options - Command options to validate
* @param {Array<string>} allowedOptions - List of allowed option names
* @returns {Object} - { valid: boolean, error?: string }
*/
export function validateCommandOptions(options, allowedOptions) {
if (!options || typeof options !== 'object') {
return { valid: true }; // Options are optional
}
const optionKeys = Object.keys(options);
const invalidOptions = optionKeys.filter(
(key) => !allowedOptions.includes(key),
);
if (invalidOptions.length > 0) {
return {
valid: false,
error: `Invalid options: ${invalidOptions.join(', ')}`,
};
}
return { valid: true };
}
/**
* Validates agent metadata
* @param {Object} metadata - Agent metadata to validate
* @returns {Object} - { valid: boolean, errors: string[] }
*/
export function validateAgentMetadata(metadata) {
const errors = [];
if (!metadata || typeof metadata !== 'object') {
return { valid: false, errors: ['Metadata must be an object'] };
}
// Required fields
const requiredFields = ['name', 'description', 'version'];
for (const field of requiredFields) {
if (!metadata[field]) {
errors.push(`Missing required field: ${field}`);
}
}
// Validate name
if (metadata.name) {
const nameValidation = validateAgentName(metadata.name);
if (!nameValidation.valid) {
errors.push(`Invalid name: ${nameValidation.error}`);
}
}
// Validate description
if (metadata.description && metadata.description.length > 200) {
errors.push('Description must be 200 characters or less');
}
// Validate version format
if (metadata.version && !/^\d+\.\d+\.\d+$/.test(metadata.version)) {
errors.push('Version must follow semantic versioning (e.g., 1.0.0)');
}
// Validate tools array
if (metadata.tools && !Array.isArray(metadata.tools)) {
errors.push('Tools must be an array');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Validates environment for safe execution
* @returns {Object} - { valid: boolean, warnings: string[] }
*/
export function validateEnvironment() {
const warnings = [];
// Check if running as root (not recommended)
if (process.getuid && process.getuid() === 0) {
warnings.push('Running as root is not recommended for security reasons');
}
// Check Node.js version
const nodeVersion = process.version;
const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1));
if (majorVersion < 16) {
warnings.push(
`Node.js ${nodeVersion} is outdated. Please upgrade to v16 or later`,
);
}
return {
valid: true,
warnings,
};
}
/**
* Escape special characters for safe display
* @param {string} text - Text to escape
* @returns {string} - Escaped text
*/
export function escapeForDisplay(text) {
if (!text || typeof text !== 'string') {
return '';
}
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}