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.
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +165 -29
- package/dist/audit.d.ts +39 -0
- package/dist/audit.js +157 -0
- package/dist/cache.d.ts +36 -0
- package/dist/cache.js +106 -0
- package/dist/cli.js +1 -1
- package/dist/history.d.ts +2 -2
- package/dist/history.js +43 -17
- package/dist/mcp.js +54 -3
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +71 -7
- package/dist/plugins.d.ts +0 -14
- package/dist/plugins.js +27 -14
- package/dist/rate-limiter.d.ts +23 -0
- package/dist/rate-limiter.js +52 -0
- package/dist/secrets.d.ts +26 -0
- package/dist/secrets.js +86 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.js +167 -0
- package/dist/tools/batch-edit.js +20 -1
- package/dist/tools/browser.js +18 -3
- package/dist/tools/code-analysis.d.ts +1 -0
- package/dist/tools/code-analysis.js +1 -0
- package/dist/tools/code-review.d.ts +1 -0
- package/dist/tools/code-review.js +1 -0
- package/dist/tools/edit.js +29 -5
- package/dist/tools/execute.js +53 -1
- package/dist/tools/glob.d.ts +1 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.d.ts +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/image-info.d.ts +1 -0
- package/dist/tools/image-info.js +1 -0
- package/dist/tools/package-manager.js +36 -0
- package/dist/tools/read.d.ts +1 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/history.js
CHANGED
|
@@ -54,31 +54,57 @@ class SessionManager {
|
|
|
54
54
|
}
|
|
55
55
|
/** Append a message to the session file */
|
|
56
56
|
save(message) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
try {
|
|
58
|
+
const line = JSON.stringify({
|
|
59
|
+
...message,
|
|
60
|
+
_ts: new Date().toISOString(),
|
|
61
|
+
_model: this.model,
|
|
62
|
+
});
|
|
63
|
+
fs.appendFileSync(this.filePath, line + '\n');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Don't crash on write failure — session persistence is best-effort
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
|
-
/** Save all messages (overwrite) */
|
|
69
|
+
/** Save all messages (atomic overwrite via temp file + rename) */
|
|
65
70
|
saveAll(messages) {
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
try {
|
|
72
|
+
const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
|
|
73
|
+
const tmpPath = this.filePath + '.tmp';
|
|
74
|
+
fs.writeFileSync(tmpPath, lines.join('\n') + '\n');
|
|
75
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Don't crash — session persistence is best-effort
|
|
79
|
+
}
|
|
68
80
|
}
|
|
69
|
-
/** Load messages from a session file */
|
|
81
|
+
/** Load messages from a session file (skips malformed lines) */
|
|
70
82
|
load() {
|
|
71
83
|
if (!fs.existsSync(this.filePath))
|
|
72
84
|
return [];
|
|
73
|
-
|
|
85
|
+
let content;
|
|
86
|
+
try {
|
|
87
|
+
content = fs.readFileSync(this.filePath, 'utf-8').trim();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
74
92
|
if (!content)
|
|
75
93
|
return [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
const messages = [];
|
|
95
|
+
for (const line of content.split('\n')) {
|
|
96
|
+
try {
|
|
97
|
+
const obj = JSON.parse(line);
|
|
98
|
+
delete obj._ts;
|
|
99
|
+
delete obj._model;
|
|
100
|
+
messages.push(obj);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Skip malformed line — don't crash the whole load
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return messages;
|
|
82
108
|
}
|
|
83
109
|
/** List recent sessions */
|
|
84
110
|
static list(limit = 10) {
|
package/dist/mcp.js
CHANGED
|
@@ -38,6 +38,37 @@ const child_process_1 = require("child_process");
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
|
+
/**
|
|
42
|
+
* MCP (Model Context Protocol) client.
|
|
43
|
+
*
|
|
44
|
+
* Connects to MCP servers defined in `.codebot/mcp.json` or `~/.codebot/mcp.json`:
|
|
45
|
+
*
|
|
46
|
+
* {
|
|
47
|
+
* "servers": [
|
|
48
|
+
* {
|
|
49
|
+
* "name": "my-server",
|
|
50
|
+
* "command": "npx",
|
|
51
|
+
* "args": ["-y", "@my/mcp-server"],
|
|
52
|
+
* "env": {}
|
|
53
|
+
* }
|
|
54
|
+
* ]
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* Each server is launched as a subprocess communicating via JSON-RPC over stdio.
|
|
58
|
+
*/
|
|
59
|
+
/** Allowlist of commands that MCP servers are permitted to run */
|
|
60
|
+
const ALLOWED_MCP_COMMANDS = new Set([
|
|
61
|
+
'npx', 'node', 'python', 'python3', 'deno', 'bun', 'docker', 'uvx',
|
|
62
|
+
]);
|
|
63
|
+
/** Safe environment variables to pass to MCP subprocesses */
|
|
64
|
+
const SAFE_ENV_VARS = new Set([
|
|
65
|
+
'PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'TMP', 'TEMP',
|
|
66
|
+
'LC_ALL', 'LC_CTYPE', 'DISPLAY', 'XDG_RUNTIME_DIR',
|
|
67
|
+
// Node.js
|
|
68
|
+
'NODE_ENV', 'NODE_PATH',
|
|
69
|
+
// Python
|
|
70
|
+
'PYTHONPATH', 'VIRTUAL_ENV',
|
|
71
|
+
]);
|
|
41
72
|
class MCPConnection {
|
|
42
73
|
process;
|
|
43
74
|
buffer = '';
|
|
@@ -46,9 +77,25 @@ class MCPConnection {
|
|
|
46
77
|
name;
|
|
47
78
|
constructor(config) {
|
|
48
79
|
this.name = config.name;
|
|
80
|
+
// Security: validate command against allowlist
|
|
81
|
+
const command = path.basename(config.command);
|
|
82
|
+
if (!ALLOWED_MCP_COMMANDS.has(command)) {
|
|
83
|
+
throw new Error(`Blocked MCP command: "${config.command}". Allowed: ${[...ALLOWED_MCP_COMMANDS].join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
// Security: build safe environment — only pass safe vars + config-defined vars
|
|
86
|
+
const safeEnv = {};
|
|
87
|
+
for (const key of SAFE_ENV_VARS) {
|
|
88
|
+
if (process.env[key]) {
|
|
89
|
+
safeEnv[key] = process.env[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Config-defined env vars override safe defaults
|
|
93
|
+
if (config.env) {
|
|
94
|
+
Object.assign(safeEnv, config.env);
|
|
95
|
+
}
|
|
49
96
|
this.process = (0, child_process_1.spawn)(config.command, config.args || [], {
|
|
50
97
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
-
env:
|
|
98
|
+
env: safeEnv,
|
|
52
99
|
});
|
|
53
100
|
this.process.stdout?.on('data', (chunk) => {
|
|
54
101
|
this.buffer += chunk.toString();
|
|
@@ -97,7 +144,7 @@ class MCPConnection {
|
|
|
97
144
|
await this.request('initialize', {
|
|
98
145
|
protocolVersion: '2024-11-05',
|
|
99
146
|
capabilities: {},
|
|
100
|
-
clientInfo: { name: 'codebot-ai', version: '1.
|
|
147
|
+
clientInfo: { name: 'codebot-ai', version: '1.6.0' },
|
|
101
148
|
});
|
|
102
149
|
await this.request('notifications/initialized');
|
|
103
150
|
}
|
|
@@ -121,9 +168,13 @@ class MCPConnection {
|
|
|
121
168
|
}
|
|
122
169
|
/** Create Tool wrappers from an MCP server's tools */
|
|
123
170
|
function mcpToolToTool(connection, def) {
|
|
171
|
+
// Security: sanitize tool description (limit length, strip control chars)
|
|
172
|
+
const safeDescription = (def.description || '')
|
|
173
|
+
.substring(0, 500)
|
|
174
|
+
.replace(/[\x00-\x1F\x7F]/g, '');
|
|
124
175
|
return {
|
|
125
176
|
name: `mcp_${connection.name}_${def.name}`,
|
|
126
|
-
description: `[MCP:${connection.name}] ${
|
|
177
|
+
description: `[MCP:${connection.name}] ${safeDescription}`,
|
|
127
178
|
permission: 'prompt',
|
|
128
179
|
parameters: def.inputSchema || { type: 'object', properties: {} },
|
|
129
180
|
execute: async (args) => {
|
package/dist/memory.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize memory content by stripping lines that look like prompt injection.
|
|
3
|
+
*/
|
|
4
|
+
export declare function sanitizeMemory(content: string): string;
|
|
1
5
|
export interface MemoryEntry {
|
|
2
6
|
key: string;
|
|
3
7
|
value: string;
|
|
@@ -8,6 +12,9 @@ export interface MemoryEntry {
|
|
|
8
12
|
* Persistent memory system for CodeBot.
|
|
9
13
|
* Stores project-level and global notes that survive across sessions.
|
|
10
14
|
* Memory is injected into the system prompt so the model always has context.
|
|
15
|
+
*
|
|
16
|
+
* Security: content is sanitized before injection to prevent prompt injection.
|
|
17
|
+
* Size limits: 2KB per file, 10KB total.
|
|
11
18
|
*/
|
|
12
19
|
export declare class MemoryManager {
|
|
13
20
|
private projectDir;
|
package/dist/memory.js
CHANGED
|
@@ -34,15 +34,57 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MemoryManager = void 0;
|
|
37
|
+
exports.sanitizeMemory = sanitizeMemory;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const os = __importStar(require("os"));
|
|
40
41
|
const MEMORY_DIR = path.join(os.homedir(), '.codebot', 'memory');
|
|
41
42
|
const GLOBAL_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
|
|
43
|
+
/** Maximum size per memory file (2KB) */
|
|
44
|
+
const MAX_FILE_SIZE = 2048;
|
|
45
|
+
/** Maximum total memory size across all files (10KB) */
|
|
46
|
+
const MAX_TOTAL_SIZE = 10240;
|
|
47
|
+
/** Patterns that indicate potential prompt injection in memory content */
|
|
48
|
+
const INJECTION_PATTERNS = [
|
|
49
|
+
/^(system|assistant|user):\s/i,
|
|
50
|
+
/ignore (previous|all|above) instructions/i,
|
|
51
|
+
/you are now/i,
|
|
52
|
+
/new instructions:/i,
|
|
53
|
+
/override:/i,
|
|
54
|
+
/<\/?system>/i,
|
|
55
|
+
/\bforget (all|everything|your)\b/i,
|
|
56
|
+
/\bact as\b/i,
|
|
57
|
+
/\brole:\s*(system|admin)/i,
|
|
58
|
+
/\bpretend (you are|to be)\b/i,
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize memory content by stripping lines that look like prompt injection.
|
|
62
|
+
*/
|
|
63
|
+
function sanitizeMemory(content) {
|
|
64
|
+
return content.split('\n')
|
|
65
|
+
.filter(line => !INJECTION_PATTERNS.some(p => p.test(line)))
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Truncate content to a maximum byte size.
|
|
70
|
+
*/
|
|
71
|
+
function truncateToSize(content, maxSize) {
|
|
72
|
+
if (Buffer.byteLength(content, 'utf-8') <= maxSize)
|
|
73
|
+
return content;
|
|
74
|
+
// Truncate by chars (approximation — will be close to byte limit)
|
|
75
|
+
let truncated = content;
|
|
76
|
+
while (Buffer.byteLength(truncated, 'utf-8') > maxSize - 50) { // leave room for marker
|
|
77
|
+
truncated = truncated.substring(0, Math.floor(truncated.length * 0.9));
|
|
78
|
+
}
|
|
79
|
+
return truncated.trimEnd() + '\n[truncated — exceeded size limit]';
|
|
80
|
+
}
|
|
42
81
|
/**
|
|
43
82
|
* Persistent memory system for CodeBot.
|
|
44
83
|
* Stores project-level and global notes that survive across sessions.
|
|
45
84
|
* Memory is injected into the system prompt so the model always has context.
|
|
85
|
+
*
|
|
86
|
+
* Security: content is sanitized before injection to prevent prompt injection.
|
|
87
|
+
* Size limits: 2KB per file, 10KB total.
|
|
46
88
|
*/
|
|
47
89
|
class MemoryManager {
|
|
48
90
|
projectDir;
|
|
@@ -76,14 +118,16 @@ class MemoryManager {
|
|
|
76
118
|
}
|
|
77
119
|
/** Write to global memory */
|
|
78
120
|
writeGlobal(content) {
|
|
79
|
-
|
|
121
|
+
const safe = truncateToSize(content, MAX_FILE_SIZE);
|
|
122
|
+
fs.writeFileSync(GLOBAL_MEMORY, safe);
|
|
80
123
|
}
|
|
81
124
|
/** Write to project memory */
|
|
82
125
|
writeProject(content) {
|
|
83
126
|
if (!this.projectDir)
|
|
84
127
|
return;
|
|
85
128
|
const memFile = path.join(this.projectDir, 'MEMORY.md');
|
|
86
|
-
|
|
129
|
+
const safe = truncateToSize(content, MAX_FILE_SIZE);
|
|
130
|
+
fs.writeFileSync(memFile, safe);
|
|
87
131
|
}
|
|
88
132
|
/** Append an entry to global memory */
|
|
89
133
|
appendGlobal(entry) {
|
|
@@ -114,20 +158,34 @@ class MemoryManager {
|
|
|
114
158
|
/** Get all memory content formatted for system prompt injection */
|
|
115
159
|
getContextBlock() {
|
|
116
160
|
const parts = [];
|
|
161
|
+
let totalSize = 0;
|
|
117
162
|
const global = this.readGlobal();
|
|
118
163
|
if (global.trim()) {
|
|
119
|
-
|
|
164
|
+
const sanitized = sanitizeMemory(global.trim());
|
|
165
|
+
const truncated = truncateToSize(sanitized, MAX_FILE_SIZE);
|
|
166
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
167
|
+
parts.push(`## Global Memory\n${truncated}`);
|
|
120
168
|
}
|
|
121
169
|
// Read additional global topic files
|
|
122
170
|
const globalFiles = this.readDir(this.globalDir);
|
|
123
171
|
for (const [name, content] of Object.entries(globalFiles)) {
|
|
124
172
|
if (name === 'MEMORY.md' || !content.trim())
|
|
125
173
|
continue;
|
|
126
|
-
|
|
174
|
+
if (totalSize >= MAX_TOTAL_SIZE)
|
|
175
|
+
break;
|
|
176
|
+
const sanitized = sanitizeMemory(content.trim());
|
|
177
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
178
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
179
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
180
|
+
parts.push(`## ${name.replace('.md', '')}\n${truncated}`);
|
|
127
181
|
}
|
|
128
182
|
const project = this.readProject();
|
|
129
|
-
if (project.trim()) {
|
|
130
|
-
|
|
183
|
+
if (project.trim() && totalSize < MAX_TOTAL_SIZE) {
|
|
184
|
+
const sanitized = sanitizeMemory(project.trim());
|
|
185
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
186
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
187
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
188
|
+
parts.push(`## Project Memory\n${truncated}`);
|
|
131
189
|
}
|
|
132
190
|
// Read additional project topic files
|
|
133
191
|
if (this.projectDir) {
|
|
@@ -135,7 +193,13 @@ class MemoryManager {
|
|
|
135
193
|
for (const [name, content] of Object.entries(projFiles)) {
|
|
136
194
|
if (name === 'MEMORY.md' || !content.trim())
|
|
137
195
|
continue;
|
|
138
|
-
|
|
196
|
+
if (totalSize >= MAX_TOTAL_SIZE)
|
|
197
|
+
break;
|
|
198
|
+
const sanitized = sanitizeMemory(content.trim());
|
|
199
|
+
const remaining = MAX_TOTAL_SIZE - totalSize;
|
|
200
|
+
const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
|
|
201
|
+
totalSize += Buffer.byteLength(truncated, 'utf-8');
|
|
202
|
+
parts.push(`## Project: ${name.replace('.md', '')}\n${truncated}`);
|
|
139
203
|
}
|
|
140
204
|
}
|
|
141
205
|
if (parts.length === 0)
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
1
|
import { Tool } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Plugin system for CodeBot.
|
|
4
|
-
*
|
|
5
|
-
* Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
|
|
6
|
-
* Each plugin exports a default function or object that implements the Tool interface:
|
|
7
|
-
*
|
|
8
|
-
* module.exports = {
|
|
9
|
-
* name: 'my_tool',
|
|
10
|
-
* description: 'Does something useful',
|
|
11
|
-
* permission: 'prompt',
|
|
12
|
-
* parameters: { type: 'object', properties: { ... }, required: [...] },
|
|
13
|
-
* execute: async (args) => { return 'result'; }
|
|
14
|
-
* };
|
|
15
|
-
*/
|
|
16
2
|
export declare function loadPlugins(projectRoot?: string): Tool[];
|
|
17
3
|
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/plugins.js
CHANGED
|
@@ -36,20 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadPlugins = loadPlugins;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
|
|
40
|
-
* Plugin system for CodeBot.
|
|
41
|
-
*
|
|
42
|
-
* Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
|
|
43
|
-
* Each plugin exports a default function or object that implements the Tool interface:
|
|
44
|
-
*
|
|
45
|
-
* module.exports = {
|
|
46
|
-
* name: 'my_tool',
|
|
47
|
-
* description: 'Does something useful',
|
|
48
|
-
* permission: 'prompt',
|
|
49
|
-
* parameters: { type: 'object', properties: { ... }, required: [...] },
|
|
50
|
-
* execute: async (args) => { return 'result'; }
|
|
51
|
-
* };
|
|
52
|
-
*/
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
53
40
|
function loadPlugins(projectRoot) {
|
|
54
41
|
const plugins = [];
|
|
55
42
|
const os = require('os');
|
|
@@ -74,6 +61,32 @@ function loadPlugins(projectRoot) {
|
|
|
74
61
|
continue;
|
|
75
62
|
try {
|
|
76
63
|
const pluginPath = path.join(dir, entry.name);
|
|
64
|
+
// Security: verify plugin against manifest hash
|
|
65
|
+
const manifestPath = path.join(dir, 'plugin.json');
|
|
66
|
+
if (!fs.existsSync(manifestPath)) {
|
|
67
|
+
console.error(`Plugin skipped (${entry.name}): no plugin.json manifest found. Create one with: { "name": "...", "version": "...", "hash": "sha256:..." }`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let manifest;
|
|
71
|
+
try {
|
|
72
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.error(`Plugin skipped (${entry.name}): invalid plugin.json manifest`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!manifest.hash || !manifest.hash.startsWith('sha256:')) {
|
|
79
|
+
console.error(`Plugin skipped (${entry.name}): manifest missing valid sha256 hash`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Compute SHA-256 of the plugin file
|
|
83
|
+
const pluginContent = fs.readFileSync(pluginPath);
|
|
84
|
+
const computedHash = 'sha256:' + crypto.createHash('sha256').update(pluginContent).digest('hex');
|
|
85
|
+
if (computedHash !== manifest.hash) {
|
|
86
|
+
console.error(`Plugin skipped (${entry.name}): hash mismatch. Expected ${manifest.hash}, got ${computedHash}. Plugin may have been tampered with.`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Hash verified — safe to load
|
|
77
90
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
78
91
|
const mod = require(pluginPath);
|
|
79
92
|
const plugin = mod.default || mod;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Simple sliding-window throttle that enforces minimum intervals between
|
|
5
|
+
* calls to the same tool. Prevents hammering web services and self-DOS.
|
|
6
|
+
*
|
|
7
|
+
* @since v1.5.0
|
|
8
|
+
*/
|
|
9
|
+
export declare class RateLimiter {
|
|
10
|
+
private lastCall;
|
|
11
|
+
/** Minimum interval (ms) between calls per tool */
|
|
12
|
+
private limits;
|
|
13
|
+
constructor(overrides?: Record<string, number>);
|
|
14
|
+
/** Wait if needed to respect the tool's rate limit */
|
|
15
|
+
throttle(toolName: string): Promise<void>;
|
|
16
|
+
/** Get the configured limit for a tool (0 = no limit) */
|
|
17
|
+
getLimit(toolName: string): number;
|
|
18
|
+
/** Update a tool's rate limit at runtime */
|
|
19
|
+
setLimit(toolName: string, intervalMs: number): void;
|
|
20
|
+
/** Reset all tracking (useful for tests) */
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-tool Rate Limiter
|
|
4
|
+
*
|
|
5
|
+
* Simple sliding-window throttle that enforces minimum intervals between
|
|
6
|
+
* calls to the same tool. Prevents hammering web services and self-DOS.
|
|
7
|
+
*
|
|
8
|
+
* @since v1.5.0
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.RateLimiter = void 0;
|
|
12
|
+
class RateLimiter {
|
|
13
|
+
lastCall = new Map();
|
|
14
|
+
/** Minimum interval (ms) between calls per tool */
|
|
15
|
+
limits = {
|
|
16
|
+
browser: 200,
|
|
17
|
+
web_fetch: 500,
|
|
18
|
+
web_search: 1000,
|
|
19
|
+
execute: 100,
|
|
20
|
+
};
|
|
21
|
+
constructor(overrides) {
|
|
22
|
+
if (overrides) {
|
|
23
|
+
Object.assign(this.limits, overrides);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Wait if needed to respect the tool's rate limit */
|
|
27
|
+
async throttle(toolName) {
|
|
28
|
+
const limit = this.limits[toolName];
|
|
29
|
+
if (!limit)
|
|
30
|
+
return; // no limit for this tool
|
|
31
|
+
const last = this.lastCall.get(toolName) || 0;
|
|
32
|
+
const elapsed = Date.now() - last;
|
|
33
|
+
if (elapsed < limit) {
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, limit - elapsed));
|
|
35
|
+
}
|
|
36
|
+
this.lastCall.set(toolName, Date.now());
|
|
37
|
+
}
|
|
38
|
+
/** Get the configured limit for a tool (0 = no limit) */
|
|
39
|
+
getLimit(toolName) {
|
|
40
|
+
return this.limits[toolName] || 0;
|
|
41
|
+
}
|
|
42
|
+
/** Update a tool's rate limit at runtime */
|
|
43
|
+
setLimit(toolName, intervalMs) {
|
|
44
|
+
this.limits[toolName] = intervalMs;
|
|
45
|
+
}
|
|
46
|
+
/** Reset all tracking (useful for tests) */
|
|
47
|
+
reset() {
|
|
48
|
+
this.lastCall.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.RateLimiter = RateLimiter;
|
|
52
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret detection module for CodeBot.
|
|
3
|
+
*
|
|
4
|
+
* Scans content for common credential patterns (API keys, tokens, passwords).
|
|
5
|
+
* Returns matches with line numbers and masked excerpts.
|
|
6
|
+
* Used to warn before writing secrets to files — does NOT block writes.
|
|
7
|
+
*/
|
|
8
|
+
export interface SecretMatch {
|
|
9
|
+
type: string;
|
|
10
|
+
line: number;
|
|
11
|
+
snippet: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scan content for secrets. Returns array of matches with line numbers and masked snippets.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanForSecrets(content: string): SecretMatch[];
|
|
17
|
+
/**
|
|
18
|
+
* Quick check: does the content contain any secrets?
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasSecrets(content: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mask secrets in an arbitrary string (e.g., for audit logging).
|
|
23
|
+
* Replaces all detected secret matches with masked versions.
|
|
24
|
+
*/
|
|
25
|
+
export declare function maskSecretsInString(text: string): string;
|
|
26
|
+
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Secret detection module for CodeBot.
|
|
4
|
+
*
|
|
5
|
+
* Scans content for common credential patterns (API keys, tokens, passwords).
|
|
6
|
+
* Returns matches with line numbers and masked excerpts.
|
|
7
|
+
* Used to warn before writing secrets to files — does NOT block writes.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.scanForSecrets = scanForSecrets;
|
|
11
|
+
exports.hasSecrets = hasSecrets;
|
|
12
|
+
exports.maskSecretsInString = maskSecretsInString;
|
|
13
|
+
const SECRET_PATTERNS = [
|
|
14
|
+
{ name: 'aws_access_key', pattern: /AKIA[0-9A-Z]{16}/ },
|
|
15
|
+
{ name: 'aws_secret_key', pattern: /(?:aws_secret_access_key|aws_secret)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/ },
|
|
16
|
+
{ name: 'private_key', pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
|
|
17
|
+
{ name: 'github_token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
|
|
18
|
+
{ name: 'github_oauth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
|
|
19
|
+
{ name: 'generic_api_key', pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
|
|
20
|
+
{ name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_\-]+/ },
|
|
21
|
+
{ name: 'password_assign', pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i },
|
|
22
|
+
{ name: 'connection_string', pattern: /(?:mongodb|postgres|postgresql|mysql|redis|amqp):\/\/[^\s]{10,}/ },
|
|
23
|
+
{ name: 'slack_token', pattern: /xox[bprs]-[0-9]{10,}-[A-Za-z0-9\-]+/ },
|
|
24
|
+
{ name: 'slack_webhook', pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/ },
|
|
25
|
+
{ name: 'generic_secret', pattern: /(?:secret|token|credential|auth_token)\s*[:=]\s*['"][^'"]{16,}['"]/i },
|
|
26
|
+
{ name: 'npm_token', pattern: /\/\/registry\.npmjs\.org\/:_authToken=[^\s]+/ },
|
|
27
|
+
{ name: 'sendgrid_key', pattern: /SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}/ },
|
|
28
|
+
{ name: 'stripe_key', pattern: /sk_(live|test)_[A-Za-z0-9]{24,}/ },
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Mask a matched secret for safe display.
|
|
32
|
+
* Shows first 4 chars + **** + last 4 chars for strings >= 12 chars.
|
|
33
|
+
* For shorter strings, shows first 2 + **** + last 2.
|
|
34
|
+
*/
|
|
35
|
+
function maskSecret(match) {
|
|
36
|
+
if (match.length >= 12) {
|
|
37
|
+
return match.substring(0, 4) + '****' + match.substring(match.length - 4);
|
|
38
|
+
}
|
|
39
|
+
if (match.length >= 6) {
|
|
40
|
+
return match.substring(0, 2) + '****' + match.substring(match.length - 2);
|
|
41
|
+
}
|
|
42
|
+
return '****';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Scan content for secrets. Returns array of matches with line numbers and masked snippets.
|
|
46
|
+
*/
|
|
47
|
+
function scanForSecrets(content) {
|
|
48
|
+
const matches = [];
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
53
|
+
const match = line.match(pattern);
|
|
54
|
+
if (match) {
|
|
55
|
+
matches.push({
|
|
56
|
+
type: name,
|
|
57
|
+
line: i + 1,
|
|
58
|
+
snippet: maskSecret(match[0]),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return matches;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Quick check: does the content contain any secrets?
|
|
67
|
+
*/
|
|
68
|
+
function hasSecrets(content) {
|
|
69
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
70
|
+
if (pattern.test(content))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Mask secrets in an arbitrary string (e.g., for audit logging).
|
|
77
|
+
* Replaces all detected secret matches with masked versions.
|
|
78
|
+
*/
|
|
79
|
+
function maskSecretsInString(text) {
|
|
80
|
+
let masked = text;
|
|
81
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
82
|
+
masked = masked.replace(new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g')), match => maskSecret(match));
|
|
83
|
+
}
|
|
84
|
+
return masked;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PathSafetyResult {
|
|
2
|
+
safe: boolean;
|
|
3
|
+
reason?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Check if a file path is safe for write/edit operations.
|
|
7
|
+
*
|
|
8
|
+
* Resolves symlinks, checks against blocked system paths,
|
|
9
|
+
* and verifies the path is within the project or user home.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isPathSafe(targetPath: string, projectRoot: string): PathSafetyResult;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a working directory is safe for command execution.
|
|
14
|
+
*
|
|
15
|
+
* Ensures the CWD exists, is a directory, and is under the project root.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCwdSafe(cwd: string, projectRoot: string): PathSafetyResult;
|
|
18
|
+
//# sourceMappingURL=security.d.ts.map
|