codebot-ai 1.5.0 → 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 +1 -0
- package/dist/agent.js +12 -0
- package/dist/audit.d.ts +39 -0
- package/dist/audit.js +157 -0
- package/dist/cli.js +1 -1
- 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/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/edit.js +29 -5
- package/dist/tools/execute.js +53 -1
- package/dist/tools/package-manager.js +36 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
package/dist/agent.js
CHANGED
|
@@ -45,6 +45,7 @@ const registry_1 = require("./providers/registry");
|
|
|
45
45
|
const plugins_1 = require("./plugins");
|
|
46
46
|
const cache_1 = require("./cache");
|
|
47
47
|
const rate_limiter_1 = require("./rate-limiter");
|
|
48
|
+
const audit_1 = require("./audit");
|
|
48
49
|
/** Lightweight schema validation — returns error string or null if valid */
|
|
49
50
|
function validateToolArgs(args, schema) {
|
|
50
51
|
const props = schema.properties;
|
|
@@ -98,6 +99,7 @@ class Agent {
|
|
|
98
99
|
model;
|
|
99
100
|
cache;
|
|
100
101
|
rateLimiter;
|
|
102
|
+
auditLogger;
|
|
101
103
|
askPermission;
|
|
102
104
|
onMessage;
|
|
103
105
|
constructor(opts) {
|
|
@@ -111,6 +113,7 @@ class Agent {
|
|
|
111
113
|
this.onMessage = opts.onMessage;
|
|
112
114
|
this.cache = new cache_1.ToolCache();
|
|
113
115
|
this.rateLimiter = new rate_limiter_1.RateLimiter();
|
|
116
|
+
this.auditLogger = new audit_1.AuditLogger();
|
|
114
117
|
// Load plugins
|
|
115
118
|
try {
|
|
116
119
|
const plugins = (0, plugins_1.loadPlugins)(process.cwd());
|
|
@@ -259,6 +262,7 @@ class Agent {
|
|
|
259
262
|
const approved = await this.askPermission(toolName, args);
|
|
260
263
|
if (!approved) {
|
|
261
264
|
denied = true;
|
|
265
|
+
this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
268
|
prepared.push({ tc, tool, args, denied });
|
|
@@ -304,6 +308,8 @@ class Agent {
|
|
|
304
308
|
await this.rateLimiter.throttle(toolName);
|
|
305
309
|
try {
|
|
306
310
|
const output = await prep.tool.execute(prep.args);
|
|
311
|
+
// Audit log: successful execution
|
|
312
|
+
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
307
313
|
// Store in cache for cacheable tools
|
|
308
314
|
if (prep.tool.cacheable) {
|
|
309
315
|
const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
|
|
@@ -315,10 +321,16 @@ class Agent {
|
|
|
315
321
|
if (filePath)
|
|
316
322
|
this.cache.invalidate(filePath);
|
|
317
323
|
}
|
|
324
|
+
// Audit log: check if tool returned a security block
|
|
325
|
+
if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
|
|
326
|
+
this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
|
|
327
|
+
}
|
|
318
328
|
return { content: output };
|
|
319
329
|
}
|
|
320
330
|
catch (err) {
|
|
321
331
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
332
|
+
// Audit log: error
|
|
333
|
+
this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
|
|
322
334
|
return { content: `Error: ${errMsg}`, is_error: true };
|
|
323
335
|
}
|
|
324
336
|
};
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit logger for CodeBot.
|
|
3
|
+
*
|
|
4
|
+
* Provides append-only JSONL logging of all security-relevant actions.
|
|
5
|
+
* Logs are stored at ~/.codebot/audit/audit-YYYY-MM-DD.jsonl
|
|
6
|
+
* Masks secrets in args before writing.
|
|
7
|
+
* NEVER throws — audit failures must not crash the agent.
|
|
8
|
+
*/
|
|
9
|
+
export interface AuditEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
tool: string;
|
|
13
|
+
action: 'execute' | 'deny' | 'error' | 'security_block';
|
|
14
|
+
args: Record<string, unknown>;
|
|
15
|
+
result?: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class AuditLogger {
|
|
19
|
+
private logDir;
|
|
20
|
+
private sessionId;
|
|
21
|
+
constructor(logDir?: string);
|
|
22
|
+
/** Get the current session ID */
|
|
23
|
+
getSessionId(): string;
|
|
24
|
+
/** Append an audit entry to the log file */
|
|
25
|
+
log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId'>): void;
|
|
26
|
+
/** Read log entries, optionally filtered */
|
|
27
|
+
query(filter?: {
|
|
28
|
+
tool?: string;
|
|
29
|
+
action?: string;
|
|
30
|
+
since?: string;
|
|
31
|
+
}): AuditEntry[];
|
|
32
|
+
/** Get the path to today's log file */
|
|
33
|
+
private getLogFilePath;
|
|
34
|
+
/** Rotate log file if it exceeds MAX_LOG_SIZE */
|
|
35
|
+
private rotateIfNeeded;
|
|
36
|
+
/** Sanitize args for logging: mask secrets and truncate long values */
|
|
37
|
+
private sanitizeArgs;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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.AuditLogger = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const secrets_1 = require("./secrets");
|
|
41
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB before rotation
|
|
42
|
+
const MAX_ARG_LENGTH = 500; // Truncate long arg values for logging
|
|
43
|
+
class AuditLogger {
|
|
44
|
+
logDir;
|
|
45
|
+
sessionId;
|
|
46
|
+
constructor(logDir) {
|
|
47
|
+
this.logDir = logDir || path.join(os.homedir(), '.codebot', 'audit');
|
|
48
|
+
this.sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Can't create dir — logging will be disabled
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Get the current session ID */
|
|
57
|
+
getSessionId() {
|
|
58
|
+
return this.sessionId;
|
|
59
|
+
}
|
|
60
|
+
/** Append an audit entry to the log file */
|
|
61
|
+
log(entry) {
|
|
62
|
+
try {
|
|
63
|
+
const fullEntry = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
sessionId: this.sessionId,
|
|
66
|
+
...entry,
|
|
67
|
+
args: this.sanitizeArgs(entry.args),
|
|
68
|
+
};
|
|
69
|
+
const logFile = this.getLogFilePath();
|
|
70
|
+
const line = JSON.stringify(fullEntry) + '\n';
|
|
71
|
+
// Check if rotation is needed
|
|
72
|
+
this.rotateIfNeeded(logFile);
|
|
73
|
+
fs.appendFileSync(logFile, line, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Audit failures must NEVER crash the agent
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Read log entries, optionally filtered */
|
|
80
|
+
query(filter) {
|
|
81
|
+
const entries = [];
|
|
82
|
+
try {
|
|
83
|
+
const files = fs.readdirSync(this.logDir)
|
|
84
|
+
.filter(f => f.startsWith('audit-') && f.endsWith('.jsonl'))
|
|
85
|
+
.sort();
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const content = fs.readFileSync(path.join(this.logDir, file), 'utf-8');
|
|
88
|
+
for (const line of content.split('\n')) {
|
|
89
|
+
if (!line.trim())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const entry = JSON.parse(line);
|
|
93
|
+
if (filter?.tool && entry.tool !== filter.tool)
|
|
94
|
+
continue;
|
|
95
|
+
if (filter?.action && entry.action !== filter.action)
|
|
96
|
+
continue;
|
|
97
|
+
if (filter?.since && entry.timestamp < filter.since)
|
|
98
|
+
continue;
|
|
99
|
+
entries.push(entry);
|
|
100
|
+
}
|
|
101
|
+
catch { /* skip malformed */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Can't read logs
|
|
107
|
+
}
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
/** Get the path to today's log file */
|
|
111
|
+
getLogFilePath() {
|
|
112
|
+
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
113
|
+
return path.join(this.logDir, `audit-${date}.jsonl`);
|
|
114
|
+
}
|
|
115
|
+
/** Rotate log file if it exceeds MAX_LOG_SIZE */
|
|
116
|
+
rotateIfNeeded(logFile) {
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(logFile))
|
|
119
|
+
return;
|
|
120
|
+
const stat = fs.statSync(logFile);
|
|
121
|
+
if (stat.size >= MAX_LOG_SIZE) {
|
|
122
|
+
const rotated = logFile.replace('.jsonl', `-${Date.now()}.jsonl`);
|
|
123
|
+
fs.renameSync(logFile, rotated);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Rotation failure is non-fatal
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Sanitize args for logging: mask secrets and truncate long values */
|
|
131
|
+
sanitizeArgs(args) {
|
|
132
|
+
const sanitized = {};
|
|
133
|
+
for (const [key, value] of Object.entries(args)) {
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
let masked = (0, secrets_1.maskSecretsInString)(value);
|
|
136
|
+
if (masked.length > MAX_ARG_LENGTH) {
|
|
137
|
+
masked = masked.substring(0, MAX_ARG_LENGTH) + `... (${value.length} chars)`;
|
|
138
|
+
}
|
|
139
|
+
sanitized[key] = masked;
|
|
140
|
+
}
|
|
141
|
+
else if (typeof value === 'object' && value !== null) {
|
|
142
|
+
// For objects/arrays, stringify and mask
|
|
143
|
+
const str = JSON.stringify(value);
|
|
144
|
+
const masked = (0, secrets_1.maskSecretsInString)(str);
|
|
145
|
+
sanitized[key] = masked.length > MAX_ARG_LENGTH
|
|
146
|
+
? masked.substring(0, MAX_ARG_LENGTH) + '...'
|
|
147
|
+
: masked;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
sanitized[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return sanitized;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.AuditLogger = AuditLogger;
|
|
157
|
+
//# sourceMappingURL=audit.js.map
|
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ const setup_1 = require("./setup");
|
|
|
44
44
|
const banner_1 = require("./banner");
|
|
45
45
|
const tools_1 = require("./tools");
|
|
46
46
|
const scheduler_1 = require("./scheduler");
|
|
47
|
-
const VERSION = '1.
|
|
47
|
+
const VERSION = '1.6.0';
|
|
48
48
|
// Session-wide token tracking
|
|
49
49
|
let sessionTokens = { input: 0, output: 0, total: 0 };
|
|
50
50
|
const C = {
|
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,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
|
package/dist/security.js
ADDED
|
@@ -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
|
package/dist/tools/batch-edit.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/tools/edit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
105
|
+
this.saveSnapshot(realPath, content);
|
|
82
106
|
const updated = content.replace(oldStr, newStr);
|
|
83
|
-
fs.writeFileSync(
|
|
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');
|
package/dist/tools/execute.js
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -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
|
}
|
package/dist/tools/web-fetch.js
CHANGED
|
@@ -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 === '
|
|
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
|
|
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
|
}
|
package/dist/tools/write.js
CHANGED
|
@@ -37,6 +37,8 @@ exports.WriteFileTool = 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
|
const UNDO_DIR = path.join(os.homedir(), '.codebot', 'undo');
|
|
41
43
|
class WriteFileTool {
|
|
42
44
|
name = 'write_file';
|
|
@@ -60,6 +62,20 @@ class WriteFileTool {
|
|
|
60
62
|
const filePath = path.resolve(args.path);
|
|
61
63
|
const content = String(args.content);
|
|
62
64
|
const dir = path.dirname(filePath);
|
|
65
|
+
// Security: path safety check
|
|
66
|
+
const projectRoot = process.cwd();
|
|
67
|
+
const safety = (0, security_1.isPathSafe)(filePath, projectRoot);
|
|
68
|
+
if (!safety.safe) {
|
|
69
|
+
return `Error: ${safety.reason}`;
|
|
70
|
+
}
|
|
71
|
+
// Security: secret detection (warn but don't block)
|
|
72
|
+
const secrets = (0, secrets_1.scanForSecrets)(content);
|
|
73
|
+
let warning = '';
|
|
74
|
+
if (secrets.length > 0) {
|
|
75
|
+
warning = `\n\n⚠️ WARNING: ${secrets.length} potential secret(s) detected:\n` +
|
|
76
|
+
secrets.map(s => ` Line ${s.line}: ${s.type} — ${s.snippet}`).join('\n') +
|
|
77
|
+
'\nConsider using environment variables instead of hardcoding secrets.';
|
|
78
|
+
}
|
|
63
79
|
if (!fs.existsSync(dir)) {
|
|
64
80
|
fs.mkdirSync(dir, { recursive: true });
|
|
65
81
|
}
|
|
@@ -74,7 +90,7 @@ class WriteFileTool {
|
|
|
74
90
|
}
|
|
75
91
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
76
92
|
const lines = content.split('\n').length;
|
|
77
|
-
return `${existed ? 'Overwrote' : 'Created'} ${filePath} (${lines} lines, ${content.length} bytes)`;
|
|
93
|
+
return `${existed ? 'Overwrote' : 'Created'} ${filePath} (${lines} lines, ${content.length} bytes)${warning}`;
|
|
78
94
|
}
|
|
79
95
|
saveSnapshot(filePath, content) {
|
|
80
96
|
try {
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebot-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Zero-dependency autonomous AI agent. Code, browse, search, automate. Works with any LLM — Ollama, Claude, GPT, Gemini, DeepSeek, Groq, Mistral, Grok.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|