codebot-ai 1.4.3 → 1.6.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.
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isPathSafe = isPathSafe;
37
+ exports.isCwdSafe = isCwdSafe;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ /**
42
+ * Path safety module for CodeBot.
43
+ *
44
+ * Prevents tools from reading/writing system-critical files and directories.
45
+ * Resolves symlinks to prevent bypass attacks.
46
+ */
47
+ /** System-critical absolute paths that should NEVER be written to */
48
+ const BLOCKED_ABSOLUTE_PATHS = [
49
+ '/etc', '/usr', '/bin', '/sbin', '/boot', '/dev', '/proc', '/sys',
50
+ '/var/log', '/var/run', '/lib', '/lib64',
51
+ // macOS system directories
52
+ '/System', '/Library',
53
+ // Windows system directories
54
+ 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
55
+ ];
56
+ /** Home-relative sensitive directories/files that should NEVER be written to */
57
+ const BLOCKED_HOME_RELATIVE = [
58
+ '.ssh',
59
+ '.gnupg',
60
+ '.aws/credentials',
61
+ '.config/gcloud',
62
+ '.bashrc',
63
+ '.bash_profile',
64
+ '.zshrc',
65
+ '.profile',
66
+ '.gitconfig',
67
+ '.npmrc',
68
+ ];
69
+ /**
70
+ * Check if a file path is safe for write/edit operations.
71
+ *
72
+ * Resolves symlinks, checks against blocked system paths,
73
+ * and verifies the path is within the project or user home.
74
+ */
75
+ function isPathSafe(targetPath, projectRoot) {
76
+ try {
77
+ const resolved = path.resolve(targetPath);
78
+ // Resolve symlinks — for new files, resolve the parent directory
79
+ let realPath;
80
+ try {
81
+ realPath = fs.realpathSync(resolved);
82
+ }
83
+ catch {
84
+ // File doesn't exist yet — resolve the parent
85
+ const parentDir = path.dirname(resolved);
86
+ try {
87
+ const realParent = fs.realpathSync(parentDir);
88
+ realPath = path.join(realParent, path.basename(resolved));
89
+ }
90
+ catch {
91
+ // Parent doesn't exist either — use the resolved path as-is
92
+ realPath = resolved;
93
+ }
94
+ }
95
+ // Check against blocked absolute paths
96
+ const normalizedPath = realPath.replace(/\\/g, '/').toLowerCase();
97
+ for (const blocked of BLOCKED_ABSOLUTE_PATHS) {
98
+ const normalizedBlocked = blocked.replace(/\\/g, '/').toLowerCase();
99
+ if (normalizedPath === normalizedBlocked || normalizedPath.startsWith(normalizedBlocked + '/')) {
100
+ return { safe: false, reason: `Blocked: "${realPath}" is inside system directory "${blocked}"` };
101
+ }
102
+ }
103
+ // Check against home-relative sensitive paths
104
+ const home = os.homedir();
105
+ for (const relative of BLOCKED_HOME_RELATIVE) {
106
+ const blockedPath = path.join(home, relative);
107
+ const normalizedBlockedHome = blockedPath.replace(/\\/g, '/').toLowerCase();
108
+ if (normalizedPath === normalizedBlockedHome || normalizedPath.startsWith(normalizedBlockedHome + '/')) {
109
+ return { safe: false, reason: `Blocked: "${realPath}" is a sensitive file/directory (~/${relative})` };
110
+ }
111
+ }
112
+ // Verify path is under project root or user home
113
+ const normalizedProject = path.resolve(projectRoot).replace(/\\/g, '/').toLowerCase();
114
+ const normalizedHome = home.replace(/\\/g, '/').toLowerCase();
115
+ const isUnderProject = normalizedPath.startsWith(normalizedProject + '/') || normalizedPath === normalizedProject;
116
+ const isUnderHome = normalizedPath.startsWith(normalizedHome + '/') || normalizedPath === normalizedHome;
117
+ if (!isUnderProject && !isUnderHome) {
118
+ return { safe: false, reason: `Blocked: "${realPath}" is outside both project root and user home directory` };
119
+ }
120
+ return { safe: true };
121
+ }
122
+ catch (err) {
123
+ return { safe: false, reason: `Path validation error: ${err instanceof Error ? err.message : String(err)}` };
124
+ }
125
+ }
126
+ /**
127
+ * Check if a working directory is safe for command execution.
128
+ *
129
+ * Ensures the CWD exists, is a directory, and is under the project root.
130
+ */
131
+ function isCwdSafe(cwd, projectRoot) {
132
+ try {
133
+ const resolved = path.resolve(cwd);
134
+ // Check it exists and is a directory
135
+ try {
136
+ const stat = fs.statSync(resolved);
137
+ if (!stat.isDirectory()) {
138
+ return { safe: false, reason: `"${resolved}" is not a directory` };
139
+ }
140
+ }
141
+ catch {
142
+ return { safe: false, reason: `Directory does not exist: "${resolved}"` };
143
+ }
144
+ // Resolve symlinks
145
+ let realPath;
146
+ try {
147
+ realPath = fs.realpathSync(resolved);
148
+ }
149
+ catch {
150
+ realPath = resolved;
151
+ }
152
+ // Verify it's under project root or user home
153
+ const normalizedPath = realPath.replace(/\\/g, '/').toLowerCase();
154
+ const normalizedProject = path.resolve(projectRoot).replace(/\\/g, '/').toLowerCase();
155
+ const normalizedHome = os.homedir().replace(/\\/g, '/').toLowerCase();
156
+ const isUnderProject = normalizedPath.startsWith(normalizedProject + '/') || normalizedPath === normalizedProject;
157
+ const isUnderHome = normalizedPath.startsWith(normalizedHome + '/') || normalizedPath === normalizedHome;
158
+ if (!isUnderProject && !isUnderHome) {
159
+ return { safe: false, reason: `CWD "${realPath}" is outside project root and user home directory` };
160
+ }
161
+ return { safe: true };
162
+ }
163
+ catch (err) {
164
+ return { safe: false, reason: `CWD validation error: ${err instanceof Error ? err.message : String(err)}` };
165
+ }
166
+ }
167
+ //# sourceMappingURL=security.js.map
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.BatchEditTool = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const security_1 = require("../security");
40
+ const secrets_1 = require("../secrets");
39
41
  class BatchEditTool {
40
42
  name = 'batch_edit';
41
43
  description = 'Apply multiple find-and-replace edits across one or more files atomically. All edits are validated before any are applied. Useful for renaming, refactoring, and coordinated multi-file changes.';
@@ -64,8 +66,10 @@ class BatchEditTool {
64
66
  if (!edits || !Array.isArray(edits) || edits.length === 0) {
65
67
  return 'Error: edits array is required and must not be empty';
66
68
  }
69
+ const projectRoot = process.cwd();
67
70
  // Phase 1: Validate all edits before applying any
68
71
  const errors = [];
72
+ const warnings = [];
69
73
  const validated = [];
70
74
  // Group edits by file so we can chain them
71
75
  const byFile = new Map();
@@ -75,6 +79,12 @@ class BatchEditTool {
75
79
  continue;
76
80
  }
77
81
  const filePath = path.resolve(edit.path);
82
+ // Security: path safety check
83
+ const safety = (0, security_1.isPathSafe)(filePath, projectRoot);
84
+ if (!safety.safe) {
85
+ errors.push(`${safety.reason}`);
86
+ continue;
87
+ }
78
88
  if (!byFile.has(filePath))
79
89
  byFile.set(filePath, []);
80
90
  byFile.get(filePath).push(edit);
@@ -98,6 +108,11 @@ class BatchEditTool {
98
108
  errors.push(`String found ${count} times in ${filePath} (must be unique): "${oldStr.substring(0, 60)}${oldStr.length > 60 ? '...' : ''}"`);
99
109
  continue;
100
110
  }
111
+ // Security: secret detection on new content
112
+ const secrets = (0, secrets_1.scanForSecrets)(newStr);
113
+ if (secrets.length > 0) {
114
+ warnings.push(`Secrets detected in edit for ${filePath}: ${secrets.map(s => `${s.type} (${s.snippet})`).join(', ')}`);
115
+ }
101
116
  content = content.replace(oldStr, newStr);
102
117
  }
103
118
  if (content !== originalContent) {
@@ -115,7 +130,11 @@ class BatchEditTool {
115
130
  }
116
131
  const fileCount = validated.length;
117
132
  const editCount = edits.length;
118
- return `Applied ${editCount} edit${editCount > 1 ? 's' : ''} across ${fileCount} file${fileCount > 1 ? 's' : ''}:\n${results.map(f => ` ✓ ${f}`).join('\n')}`;
133
+ let output = `Applied ${editCount} edit${editCount > 1 ? 's' : ''} across ${fileCount} file${fileCount > 1 ? 's' : ''}:\n${results.map(f => ` ✓ ${f}`).join('\n')}`;
134
+ if (warnings.length > 0) {
135
+ output += `\n\n⚠️ Security warnings:\n${warnings.map(w => ` - ${w}`).join('\n')}`;
136
+ }
137
+ return output;
119
138
  }
120
139
  }
121
140
  exports.BatchEditTool = BatchEditTool;
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
42
42
  // Shared browser instance across tool calls
43
43
  let client = null;
44
44
  let debugPort = 9222;
45
+ let connectingPromise = null;
45
46
  const CHROME_DATA_DIR = path.join(os.homedir(), '.codebot', 'chrome-profile');
46
47
  /** Kill any Chrome using our debug port or data dir (but NEVER kill ourselves) */
47
48
  function killExistingChrome() {
@@ -71,8 +72,21 @@ function killExistingChrome() {
71
72
  }
72
73
  }
73
74
  async function ensureConnected() {
75
+ // Fast path: already connected
74
76
  if (client?.isConnected())
75
77
  return client;
78
+ // Mutex: if another call is already connecting, reuse that promise
79
+ if (connectingPromise)
80
+ return connectingPromise;
81
+ connectingPromise = doConnect();
82
+ try {
83
+ return await connectingPromise;
84
+ }
85
+ finally {
86
+ connectingPromise = null;
87
+ }
88
+ }
89
+ async function doConnect() {
76
90
  // Try connecting to existing Chrome with debug port
77
91
  try {
78
92
  const wsUrl = await (0, cdp_1.getDebuggerUrl)(debugPort);
@@ -172,10 +186,11 @@ async function ensureConnected() {
172
186
  'Or on macOS:\n' +
173
187
  ` /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=${debugPort}`);
174
188
  }
175
- // Wait for Chrome to start
176
- for (let i = 0; i < 10; i++) {
189
+ // Wait for Chrome to start — exponential backoff: 500ms, 1s, 2s, 4s
190
+ const backoffDelays = [500, 1000, 2000, 4000, 4000, 4000];
191
+ for (let i = 0; i < backoffDelays.length; i++) {
177
192
  try {
178
- await new Promise(r => setTimeout(r, 500));
193
+ await new Promise(r => setTimeout(r, backoffDelays[i]));
179
194
  const wsUrl = await (0, cdp_1.getDebuggerUrl)(debugPort);
180
195
  client = new cdp_1.CDPClient();
181
196
  await client.connect(wsUrl);
@@ -3,6 +3,7 @@ export declare class CodeAnalysisTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -40,6 +40,7 @@ class CodeAnalysisTool {
40
40
  name = 'code_analysis';
41
41
  description = 'Analyze code structure. Actions: symbols (list classes/functions/exports), imports (list imports), outline (file structure), references (find where a symbol is used).';
42
42
  permission = 'auto';
43
+ cacheable = true;
43
44
  parameters = {
44
45
  type: 'object',
45
46
  properties: {
@@ -3,6 +3,7 @@ export declare class CodeReviewTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -52,6 +52,7 @@ class CodeReviewTool {
52
52
  name = 'code_review';
53
53
  description = 'Review code for security issues, complexity, and code smells. Actions: security, complexity, review (full).';
54
54
  permission = 'auto';
55
+ cacheable = true;
55
56
  parameters = {
56
57
  type: 'object',
57
58
  properties: {
@@ -37,6 +37,8 @@ exports.EditFileTool = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
+ const security_1 = require("../security");
41
+ const secrets_1 = require("../secrets");
40
42
  // Undo snapshot directory
41
43
  const UNDO_DIR = path.join(os.homedir(), '.codebot', 'undo');
42
44
  const MAX_UNDO = 50;
@@ -66,10 +68,32 @@ class EditFileTool {
66
68
  const filePath = path.resolve(args.path);
67
69
  const oldStr = String(args.old_string);
68
70
  const newStr = String(args.new_string);
69
- if (!fs.existsSync(filePath)) {
71
+ // Security: path safety check
72
+ const projectRoot = process.cwd();
73
+ const safety = (0, security_1.isPathSafe)(filePath, projectRoot);
74
+ if (!safety.safe) {
75
+ return `Error: ${safety.reason}`;
76
+ }
77
+ // Security: resolve symlinks before reading
78
+ let realPath;
79
+ try {
80
+ realPath = fs.realpathSync(filePath);
81
+ }
82
+ catch {
70
83
  throw new Error(`File not found: ${filePath}`);
71
84
  }
72
- const content = fs.readFileSync(filePath, 'utf-8');
85
+ if (!fs.existsSync(realPath)) {
86
+ throw new Error(`File not found: ${filePath}`);
87
+ }
88
+ // Security: secret detection on new content (warn but don't block)
89
+ const secrets = (0, secrets_1.scanForSecrets)(newStr);
90
+ let warning = '';
91
+ if (secrets.length > 0) {
92
+ warning = `\n\n⚠️ WARNING: ${secrets.length} potential secret(s) in new content:\n` +
93
+ secrets.map(s => ` ${s.type} — ${s.snippet}`).join('\n') +
94
+ '\nConsider using environment variables instead of hardcoding secrets.';
95
+ }
96
+ const content = fs.readFileSync(realPath, 'utf-8');
73
97
  const count = content.split(oldStr).length - 1;
74
98
  if (count === 0) {
75
99
  throw new Error(`String not found in ${filePath}. Make sure old_string matches exactly (including whitespace).`);
@@ -78,12 +102,12 @@ class EditFileTool {
78
102
  throw new Error(`String found ${count} times in ${filePath}. Provide more surrounding context to make it unique.`);
79
103
  }
80
104
  // Save undo snapshot
81
- this.saveSnapshot(filePath, content);
105
+ this.saveSnapshot(realPath, content);
82
106
  const updated = content.replace(oldStr, newStr);
83
- fs.writeFileSync(filePath, updated, 'utf-8');
107
+ fs.writeFileSync(realPath, updated, 'utf-8');
84
108
  // Generate diff preview
85
109
  const diff = this.generateDiff(oldStr, newStr, content, filePath);
86
- return diff;
110
+ return diff + warning;
87
111
  }
88
112
  generateDiff(oldStr, newStr, content, filePath) {
89
113
  const lines = content.split('\n');
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExecuteTool = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ const security_1 = require("../security");
5
6
  const BLOCKED_PATTERNS = [
6
7
  // Destructive filesystem operations
7
8
  /rm\s+-rf\s+\//,
@@ -39,6 +40,44 @@ const BLOCKED_PATTERNS = [
39
40
  /insmod\b/,
40
41
  /rmmod\b/,
41
42
  /modprobe\s+-r/,
43
+ // ── v1.6.0 security hardening: evasion-resistant patterns ──
44
+ // Base64 decode pipes (obfuscated command execution)
45
+ /base64\s+(-d|--decode)\s*\|/,
46
+ // Hex escape sequences (obfuscation)
47
+ /\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}/,
48
+ // Variable-based obfuscation
49
+ /\$\{[^}]*rm\s/,
50
+ /eval\s+.*\$/,
51
+ // Backtick-based command injection
52
+ /`[^`]*rm\s+-rf/,
53
+ // Process substitution with dangerous commands
54
+ /<\(.*curl/,
55
+ /<\(.*wget/,
56
+ // Python/perl inline execution of destructive commands
57
+ /python[23]?\s+-c\s+.*import\s+os.*remove/,
58
+ /perl\s+-e\s+.*unlink/,
59
+ // Encoded shell commands
60
+ /echo\s+.*\|\s*base64\s+(-d|--decode)\s*\|\s*(ba)?sh/,
61
+ // Crontab manipulation
62
+ /crontab\s+-r/,
63
+ // Systemctl destructive operations
64
+ /systemctl\s+(disable|mask|stop)\s+(sshd|firewalld|iptables)/,
65
+ ];
66
+ /** Sensitive environment variables to strip before passing to child process */
67
+ const FILTERED_ENV_VARS = [
68
+ 'AWS_SECRET_ACCESS_KEY',
69
+ 'AWS_SESSION_TOKEN',
70
+ 'GITHUB_TOKEN',
71
+ 'GH_TOKEN',
72
+ 'NPM_TOKEN',
73
+ 'DATABASE_URL',
74
+ 'OPENAI_API_KEY',
75
+ 'ANTHROPIC_API_KEY',
76
+ 'GOOGLE_API_KEY',
77
+ 'STRIPE_SECRET_KEY',
78
+ 'SENDGRID_API_KEY',
79
+ 'SLACK_TOKEN',
80
+ 'SLACK_BOT_TOKEN',
42
81
  ];
43
82
  class ExecuteTool {
44
83
  name = 'execute';
@@ -63,13 +102,26 @@ class ExecuteTool {
63
102
  throw new Error(`Blocked: "${cmd}" matches a dangerous command pattern.`);
64
103
  }
65
104
  }
105
+ // Security: validate CWD
106
+ const cwd = args.cwd || process.cwd();
107
+ const projectRoot = process.cwd();
108
+ const cwdSafety = (0, security_1.isCwdSafe)(cwd, projectRoot);
109
+ if (!cwdSafety.safe) {
110
+ return `Error: ${cwdSafety.reason}`;
111
+ }
112
+ // Security: filter sensitive env vars
113
+ const safeEnv = { ...process.env };
114
+ for (const key of FILTERED_ENV_VARS) {
115
+ delete safeEnv[key];
116
+ }
66
117
  try {
67
118
  const output = (0, child_process_1.execSync)(cmd, {
68
- cwd: args.cwd || process.cwd(),
119
+ cwd,
69
120
  timeout: args.timeout || 30000,
70
121
  maxBuffer: 1024 * 1024,
71
122
  encoding: 'utf-8',
72
123
  stdio: ['pipe', 'pipe', 'pipe'],
124
+ env: safeEnv,
73
125
  });
74
126
  return output || '(no output)';
75
127
  }
@@ -3,6 +3,7 @@ export declare class GlobTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -40,6 +40,7 @@ class GlobTool {
40
40
  name = 'glob';
41
41
  description = 'Find files matching a glob pattern. Returns matching file paths relative to the search directory.';
42
42
  permission = 'auto';
43
+ cacheable = true;
43
44
  parameters = {
44
45
  type: 'object',
45
46
  properties: {
@@ -3,6 +3,7 @@ export declare class GrepTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -40,6 +40,7 @@ class GrepTool {
40
40
  name = 'grep';
41
41
  description = 'Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.';
42
42
  permission = 'auto';
43
+ cacheable = true;
43
44
  parameters = {
44
45
  type: 'object',
45
46
  properties: {
@@ -3,6 +3,7 @@ export declare class ImageInfoTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -40,6 +40,7 @@ class ImageInfoTool {
40
40
  name = 'image_info';
41
41
  description = 'Get image file information — dimensions, format, file size. Supports PNG, JPEG, GIF, BMP, SVG.';
42
42
  permission = 'auto';
43
+ cacheable = true;
43
44
  parameters = {
44
45
  type: 'object',
45
46
  properties: {
@@ -63,6 +63,34 @@ const MANAGERS = {
63
63
  remove: 'go mod tidy', list: 'go list -m all', outdated: 'go list -m -u all', audit: 'govulncheck ./...',
64
64
  },
65
65
  };
66
+ /**
67
+ * Package name validation patterns by ecosystem.
68
+ * Rejects names containing shell metacharacters or injection attempts.
69
+ */
70
+ const SAFE_PKG_PATTERNS = {
71
+ // npm: @scope/pkg@version or pkg@version
72
+ npm: /^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*(@[a-z0-9^~>=<.*\-]+)?$/,
73
+ yarn: /^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*(@[a-z0-9^~>=<.*\-]+)?$/,
74
+ pnpm: /^(@[a-z0-9\-~][a-z0-9\-._~]*\/)?[a-z0-9\-~][a-z0-9\-._~]*(@[a-z0-9^~>=<.*\-]+)?$/,
75
+ // pip: allows hyphens, underscores, dots, optional version spec
76
+ pip: /^[a-zA-Z0-9][a-zA-Z0-9._\-]*(\[[a-zA-Z0-9,._\-]+\])?(([>=<!=~]+)[a-zA-Z0-9.*]+)?$/,
77
+ // cargo: lowercase alphanumeric + hyphens + underscores
78
+ cargo: /^[a-zA-Z][a-zA-Z0-9_\-]*(@[a-zA-Z0-9.^~>=<*\-]+)?$/,
79
+ // go: module paths
80
+ go: /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*(@[a-zA-Z0-9.^~>=<*\-]+)?$/,
81
+ };
82
+ function isPackageNameSafe(pkgName, manager) {
83
+ // Split by spaces to handle multiple package args
84
+ const packages = pkgName.trim().split(/\s+/);
85
+ const pattern = SAFE_PKG_PATTERNS[manager];
86
+ if (!pattern)
87
+ return false;
88
+ for (const pkg of packages) {
89
+ if (!pattern.test(pkg))
90
+ return false;
91
+ }
92
+ return true;
93
+ }
66
94
  class PackageManagerTool {
67
95
  name = 'package_manager';
68
96
  description = 'Manage dependencies. Auto-detects npm/yarn/pnpm/pip/cargo/go. Actions: install, add, remove, list, outdated, audit, detect.';
@@ -98,6 +126,10 @@ class PackageManagerTool {
98
126
  const pkg = args.package;
99
127
  if (!pkg)
100
128
  return 'Error: package name is required for add';
129
+ // Security: validate package name
130
+ if (!isPackageNameSafe(pkg, mgr.name)) {
131
+ return `Error: invalid package name "${pkg}". Package names must be alphanumeric with hyphens/underscores/dots only. Shell metacharacters are not allowed.`;
132
+ }
101
133
  cmd = `${mgr.add} ${pkg}`;
102
134
  break;
103
135
  }
@@ -105,6 +137,10 @@ class PackageManagerTool {
105
137
  const pkg = args.package;
106
138
  if (!pkg)
107
139
  return 'Error: package name is required for remove';
140
+ // Security: validate package name
141
+ if (!isPackageNameSafe(pkg, mgr.name)) {
142
+ return `Error: invalid package name "${pkg}". Package names must be alphanumeric with hyphens/underscores/dots only. Shell metacharacters are not allowed.`;
143
+ }
108
144
  cmd = `${mgr.remove} ${pkg}`;
109
145
  break;
110
146
  }
@@ -3,6 +3,7 @@ export declare class ReadFileTool implements Tool {
3
3
  name: string;
4
4
  description: string;
5
5
  permission: Tool['permission'];
6
+ cacheable: boolean;
6
7
  parameters: {
7
8
  type: string;
8
9
  properties: {
@@ -40,6 +40,7 @@ class ReadFileTool {
40
40
  name = 'read_file';
41
41
  description = 'Read the contents of a file. Returns file content with line numbers.';
42
42
  permission = 'auto';
43
+ cacheable = true;
43
44
  parameters = {
44
45
  type: 'object',
45
46
  properties: {
@@ -31,17 +31,19 @@ class WebFetchTool {
31
31
  // Block requests to private/internal IPs
32
32
  const hostname = parsed.hostname.toLowerCase();
33
33
  // Block localhost variants
34
- if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0') {
34
+ if (hostname === 'localhost' || hostname === '::1' || hostname === '0.0.0.0') {
35
35
  return 'Blocked: requests to localhost are not allowed';
36
36
  }
37
37
  // Block cloud metadata endpoints
38
38
  if (hostname === '169.254.169.254' || hostname === 'metadata.google.internal') {
39
39
  return 'Blocked: requests to cloud metadata endpoints are not allowed';
40
40
  }
41
- // Block private IP ranges (10.x, 172.16-31.x, 192.168.x)
41
+ // Block private IPv4 ranges
42
42
  const ipMatch = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
43
43
  if (ipMatch) {
44
44
  const [, a, b] = ipMatch.map(Number);
45
+ if (a === 127)
46
+ return 'Blocked: loopback IP range (127.x.x.x)'; // Full 127.0.0.0/8
45
47
  if (a === 10)
46
48
  return 'Blocked: private IP range (10.x.x.x)';
47
49
  if (a === 172 && b >= 16 && b <= 31)
@@ -50,6 +52,44 @@ class WebFetchTool {
50
52
  return 'Blocked: private IP range (192.168.x.x)';
51
53
  if (a === 0)
52
54
  return 'Blocked: invalid IP (0.x.x.x)';
55
+ if (a === 169 && b === 254)
56
+ return 'Blocked: link-local IP (169.254.x.x)';
57
+ }
58
+ // ── v1.6.0 security hardening: IPv6 private range blocking ──
59
+ // Remove brackets for IPv6 addresses
60
+ const bare = hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
61
+ // IPv6 loopback
62
+ if (bare === '::1' || bare === '0:0:0:0:0:0:0:1') {
63
+ return 'Blocked: IPv6 loopback (::1)';
64
+ }
65
+ // IPv6 link-local (fe80::/10)
66
+ if (/^fe[89ab][0-9a-f]:/i.test(bare)) {
67
+ return 'Blocked: IPv6 link-local address (fe80::/10)';
68
+ }
69
+ // IPv6 unique local address (fc00::/7 — includes fd00::/8)
70
+ if (/^f[cd][0-9a-f]{2}:/i.test(bare)) {
71
+ return 'Blocked: IPv6 unique local address (fc00::/7)';
72
+ }
73
+ // IPv6 multicast (ff00::/8)
74
+ if (/^ff[0-9a-f]{2}:/i.test(bare)) {
75
+ return 'Blocked: IPv6 multicast address (ff00::/8)';
76
+ }
77
+ // IPv6-mapped IPv4 addresses (::ffff:x.x.x.x)
78
+ const mappedMatch = bare.match(/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
79
+ if (mappedMatch) {
80
+ const [, a, b] = mappedMatch.map(Number);
81
+ if (a === 127)
82
+ return 'Blocked: IPv4-mapped loopback';
83
+ if (a === 10)
84
+ return 'Blocked: IPv4-mapped private IP';
85
+ if (a === 172 && b >= 16 && b <= 31)
86
+ return 'Blocked: IPv4-mapped private IP';
87
+ if (a === 192 && b === 168)
88
+ return 'Blocked: IPv4-mapped private IP';
89
+ if (a === 0)
90
+ return 'Blocked: IPv4-mapped invalid IP';
91
+ if (a === 169 && b === 254)
92
+ return 'Blocked: IPv4-mapped link-local';
53
93
  }
54
94
  return null; // URL is safe
55
95
  }