codebot-ai 1.5.0 → 1.7.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 +13 -0
- package/dist/agent.js +63 -4
- package/dist/audit.d.ts +61 -0
- package/dist/audit.js +231 -0
- package/dist/cli.js +181 -26
- 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/policy.d.ts +123 -0
- package/dist/policy.js +418 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +214 -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/telemetry.d.ts +73 -0
- package/dist/telemetry.js +286 -0
- package/dist/tools/batch-edit.js +20 -1
- package/dist/tools/edit.js +29 -5
- package/dist/tools/execute.js +81 -4
- 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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token & Cost Tracking for CodeBot v1.7.0
|
|
4
|
+
*
|
|
5
|
+
* Tracks per-request and per-session token usage and estimated costs.
|
|
6
|
+
* Supports cost limits and historical usage queries.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.TokenTracker = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const PRICING = {
|
|
47
|
+
// Anthropic
|
|
48
|
+
'claude-opus-4-6': { input: 15.0, output: 75.0 },
|
|
49
|
+
'claude-sonnet-4-20250514': { input: 3.0, output: 15.0 },
|
|
50
|
+
'claude-haiku-3-5': { input: 0.80, output: 4.0 },
|
|
51
|
+
// OpenAI
|
|
52
|
+
'gpt-4o': { input: 2.50, output: 10.0 },
|
|
53
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
54
|
+
'gpt-4.1': { input: 2.0, output: 8.0 },
|
|
55
|
+
'gpt-4.1-mini': { input: 0.40, output: 1.60 },
|
|
56
|
+
'gpt-4.1-nano': { input: 0.10, output: 0.40 },
|
|
57
|
+
'o1': { input: 15.0, output: 60.0 },
|
|
58
|
+
'o3': { input: 10.0, output: 40.0 },
|
|
59
|
+
'o3-mini': { input: 1.10, output: 4.40 },
|
|
60
|
+
'o4-mini': { input: 1.10, output: 4.40 },
|
|
61
|
+
// Google
|
|
62
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.0 },
|
|
63
|
+
'gemini-2.5-flash': { input: 0.15, output: 0.60 },
|
|
64
|
+
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
65
|
+
// DeepSeek
|
|
66
|
+
'deepseek-chat': { input: 0.14, output: 0.28 },
|
|
67
|
+
'deepseek-reasoner': { input: 0.55, output: 2.19 },
|
|
68
|
+
// Groq (free tier pricing)
|
|
69
|
+
'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
|
|
70
|
+
// Mistral
|
|
71
|
+
'mistral-large-latest': { input: 2.0, output: 6.0 },
|
|
72
|
+
'codestral-latest': { input: 0.30, output: 0.90 },
|
|
73
|
+
// xAI
|
|
74
|
+
'grok-3': { input: 3.0, output: 15.0 },
|
|
75
|
+
'grok-3-mini': { input: 0.30, output: 0.50 },
|
|
76
|
+
};
|
|
77
|
+
// Default pricing for unknown models (conservative estimate)
|
|
78
|
+
const DEFAULT_PRICING = { input: 2.0, output: 8.0 };
|
|
79
|
+
// Free pricing for local models
|
|
80
|
+
const LOCAL_PRICING = { input: 0, output: 0 };
|
|
81
|
+
class TokenTracker {
|
|
82
|
+
model;
|
|
83
|
+
provider;
|
|
84
|
+
sessionId;
|
|
85
|
+
records = [];
|
|
86
|
+
toolCallCount = 0;
|
|
87
|
+
filesModifiedSet = new Set();
|
|
88
|
+
startTime;
|
|
89
|
+
costLimitUsd = 0;
|
|
90
|
+
constructor(model, provider, sessionId) {
|
|
91
|
+
this.model = model;
|
|
92
|
+
this.provider = provider;
|
|
93
|
+
this.sessionId = sessionId || `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
94
|
+
this.startTime = new Date().toISOString();
|
|
95
|
+
}
|
|
96
|
+
/** Set cost limit in USD. 0 = no limit. */
|
|
97
|
+
setCostLimit(usd) {
|
|
98
|
+
this.costLimitUsd = usd;
|
|
99
|
+
}
|
|
100
|
+
/** Record token usage from an LLM request */
|
|
101
|
+
recordUsage(inputTokens, outputTokens) {
|
|
102
|
+
const pricing = this.getPricing();
|
|
103
|
+
const costUsd = (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
104
|
+
const record = {
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
model: this.model,
|
|
107
|
+
provider: this.provider,
|
|
108
|
+
inputTokens,
|
|
109
|
+
outputTokens,
|
|
110
|
+
costUsd,
|
|
111
|
+
};
|
|
112
|
+
this.records.push(record);
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
/** Record a tool call (for summary) */
|
|
116
|
+
recordToolCall() {
|
|
117
|
+
this.toolCallCount++;
|
|
118
|
+
}
|
|
119
|
+
/** Record a file modification (for summary) */
|
|
120
|
+
recordFileModified(filePath) {
|
|
121
|
+
this.filesModifiedSet.add(filePath);
|
|
122
|
+
}
|
|
123
|
+
/** Check if cost limit has been exceeded */
|
|
124
|
+
isOverBudget() {
|
|
125
|
+
if (this.costLimitUsd <= 0)
|
|
126
|
+
return false;
|
|
127
|
+
return this.getTotalCost() >= this.costLimitUsd;
|
|
128
|
+
}
|
|
129
|
+
/** Get remaining budget in USD (Infinity if no limit) */
|
|
130
|
+
getRemainingBudget() {
|
|
131
|
+
if (this.costLimitUsd <= 0)
|
|
132
|
+
return Infinity;
|
|
133
|
+
return Math.max(0, this.costLimitUsd - this.getTotalCost());
|
|
134
|
+
}
|
|
135
|
+
// ── Aggregates ──
|
|
136
|
+
getTotalInputTokens() {
|
|
137
|
+
return this.records.reduce((sum, r) => sum + r.inputTokens, 0);
|
|
138
|
+
}
|
|
139
|
+
getTotalOutputTokens() {
|
|
140
|
+
return this.records.reduce((sum, r) => sum + r.outputTokens, 0);
|
|
141
|
+
}
|
|
142
|
+
getTotalCost() {
|
|
143
|
+
return this.records.reduce((sum, r) => sum + r.costUsd, 0);
|
|
144
|
+
}
|
|
145
|
+
getRequestCount() {
|
|
146
|
+
return this.records.length;
|
|
147
|
+
}
|
|
148
|
+
/** Generate a session summary */
|
|
149
|
+
getSummary() {
|
|
150
|
+
return {
|
|
151
|
+
sessionId: this.sessionId,
|
|
152
|
+
model: this.model,
|
|
153
|
+
provider: this.provider,
|
|
154
|
+
startTime: this.startTime,
|
|
155
|
+
endTime: new Date().toISOString(),
|
|
156
|
+
totalInputTokens: this.getTotalInputTokens(),
|
|
157
|
+
totalOutputTokens: this.getTotalOutputTokens(),
|
|
158
|
+
totalCostUsd: this.getTotalCost(),
|
|
159
|
+
requestCount: this.getRequestCount(),
|
|
160
|
+
toolCalls: this.toolCallCount,
|
|
161
|
+
filesModified: this.filesModifiedSet.size,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Format cost for display */
|
|
165
|
+
formatCost() {
|
|
166
|
+
const cost = this.getTotalCost();
|
|
167
|
+
if (cost === 0)
|
|
168
|
+
return 'free (local model)';
|
|
169
|
+
if (cost < 0.01)
|
|
170
|
+
return `< $0.01`;
|
|
171
|
+
return `$${cost.toFixed(4)}`;
|
|
172
|
+
}
|
|
173
|
+
/** Format a compact status line for CLI */
|
|
174
|
+
formatStatusLine() {
|
|
175
|
+
const inTk = this.getTotalInputTokens();
|
|
176
|
+
const outTk = this.getTotalOutputTokens();
|
|
177
|
+
const cost = this.formatCost();
|
|
178
|
+
return `${inTk.toLocaleString()} in / ${outTk.toLocaleString()} out | ${cost}`;
|
|
179
|
+
}
|
|
180
|
+
/** Save session usage to ~/.codebot/usage/ for historical tracking */
|
|
181
|
+
saveUsage() {
|
|
182
|
+
try {
|
|
183
|
+
const usageDir = path.join(os.homedir(), '.codebot', 'usage');
|
|
184
|
+
fs.mkdirSync(usageDir, { recursive: true });
|
|
185
|
+
const summary = this.getSummary();
|
|
186
|
+
const fileName = `usage-${summary.startTime.split('T')[0]}.jsonl`;
|
|
187
|
+
const filePath = path.join(usageDir, fileName);
|
|
188
|
+
fs.appendFileSync(filePath, JSON.stringify(summary) + '\n', 'utf-8');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Usage tracking failures are non-fatal
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load historical usage from ~/.codebot/usage/
|
|
196
|
+
*/
|
|
197
|
+
static loadHistory(days) {
|
|
198
|
+
const summaries = [];
|
|
199
|
+
try {
|
|
200
|
+
const usageDir = path.join(os.homedir(), '.codebot', 'usage');
|
|
201
|
+
if (!fs.existsSync(usageDir))
|
|
202
|
+
return [];
|
|
203
|
+
const files = fs.readdirSync(usageDir)
|
|
204
|
+
.filter(f => f.startsWith('usage-') && f.endsWith('.jsonl'))
|
|
205
|
+
.sort();
|
|
206
|
+
// Filter by date range if specified
|
|
207
|
+
const cutoff = days
|
|
208
|
+
? new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
209
|
+
: undefined;
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const content = fs.readFileSync(path.join(usageDir, file), 'utf-8');
|
|
212
|
+
for (const line of content.split('\n')) {
|
|
213
|
+
if (!line.trim())
|
|
214
|
+
continue;
|
|
215
|
+
try {
|
|
216
|
+
const summary = JSON.parse(line);
|
|
217
|
+
if (cutoff && summary.startTime < cutoff)
|
|
218
|
+
continue;
|
|
219
|
+
summaries.push(summary);
|
|
220
|
+
}
|
|
221
|
+
catch { /* skip malformed */ }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Can't read usage
|
|
227
|
+
}
|
|
228
|
+
return summaries;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Format a historical usage report.
|
|
232
|
+
*/
|
|
233
|
+
static formatUsageReport(days = 30) {
|
|
234
|
+
const history = TokenTracker.loadHistory(days);
|
|
235
|
+
if (history.length === 0)
|
|
236
|
+
return 'No usage data found.';
|
|
237
|
+
let totalInput = 0;
|
|
238
|
+
let totalOutput = 0;
|
|
239
|
+
let totalCost = 0;
|
|
240
|
+
let totalRequests = 0;
|
|
241
|
+
let totalTools = 0;
|
|
242
|
+
for (const s of history) {
|
|
243
|
+
totalInput += s.totalInputTokens;
|
|
244
|
+
totalOutput += s.totalOutputTokens;
|
|
245
|
+
totalCost += s.totalCostUsd;
|
|
246
|
+
totalRequests += s.requestCount;
|
|
247
|
+
totalTools += s.toolCalls;
|
|
248
|
+
}
|
|
249
|
+
const lines = [
|
|
250
|
+
`Usage Report (last ${days} days)`,
|
|
251
|
+
'─'.repeat(40),
|
|
252
|
+
`Sessions: ${history.length}`,
|
|
253
|
+
`LLM Requests: ${totalRequests.toLocaleString()}`,
|
|
254
|
+
`Tool Calls: ${totalTools.toLocaleString()}`,
|
|
255
|
+
`Input Tokens: ${totalInput.toLocaleString()}`,
|
|
256
|
+
`Output Tokens: ${totalOutput.toLocaleString()}`,
|
|
257
|
+
`Total Cost: $${totalCost.toFixed(4)}`,
|
|
258
|
+
];
|
|
259
|
+
return lines.join('\n');
|
|
260
|
+
}
|
|
261
|
+
// ── Helpers ──
|
|
262
|
+
getPricing() {
|
|
263
|
+
// Local models are free
|
|
264
|
+
if (this.isLocalModel())
|
|
265
|
+
return LOCAL_PRICING;
|
|
266
|
+
// Exact match
|
|
267
|
+
if (PRICING[this.model])
|
|
268
|
+
return PRICING[this.model];
|
|
269
|
+
// Prefix match (for versioned models like claude-sonnet-4-20250514)
|
|
270
|
+
for (const [key, pricing] of Object.entries(PRICING)) {
|
|
271
|
+
if (this.model.startsWith(key))
|
|
272
|
+
return pricing;
|
|
273
|
+
}
|
|
274
|
+
return DEFAULT_PRICING;
|
|
275
|
+
}
|
|
276
|
+
isLocalModel() {
|
|
277
|
+
// Models running on Ollama, LM Studio, vLLM are free
|
|
278
|
+
return !this.provider ||
|
|
279
|
+
this.provider === 'ollama' ||
|
|
280
|
+
this.provider === 'lmstudio' ||
|
|
281
|
+
this.provider === 'vllm' ||
|
|
282
|
+
this.provider === 'local';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exports.TokenTracker = TokenTracker;
|
|
286
|
+
//# sourceMappingURL=telemetry.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,9 @@
|
|
|
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");
|
|
6
|
+
const sandbox_1 = require("../sandbox");
|
|
7
|
+
const policy_1 = require("../policy");
|
|
5
8
|
const BLOCKED_PATTERNS = [
|
|
6
9
|
// Destructive filesystem operations
|
|
7
10
|
/rm\s+-rf\s+\//,
|
|
@@ -39,6 +42,44 @@ const BLOCKED_PATTERNS = [
|
|
|
39
42
|
/insmod\b/,
|
|
40
43
|
/rmmod\b/,
|
|
41
44
|
/modprobe\s+-r/,
|
|
45
|
+
// ── v1.6.0 security hardening: evasion-resistant patterns ──
|
|
46
|
+
// Base64 decode pipes (obfuscated command execution)
|
|
47
|
+
/base64\s+(-d|--decode)\s*\|/,
|
|
48
|
+
// Hex escape sequences (obfuscation)
|
|
49
|
+
/\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}/,
|
|
50
|
+
// Variable-based obfuscation
|
|
51
|
+
/\$\{[^}]*rm\s/,
|
|
52
|
+
/eval\s+.*\$/,
|
|
53
|
+
// Backtick-based command injection
|
|
54
|
+
/`[^`]*rm\s+-rf/,
|
|
55
|
+
// Process substitution with dangerous commands
|
|
56
|
+
/<\(.*curl/,
|
|
57
|
+
/<\(.*wget/,
|
|
58
|
+
// Python/perl inline execution of destructive commands
|
|
59
|
+
/python[23]?\s+-c\s+.*import\s+os.*remove/,
|
|
60
|
+
/perl\s+-e\s+.*unlink/,
|
|
61
|
+
// Encoded shell commands
|
|
62
|
+
/echo\s+.*\|\s*base64\s+(-d|--decode)\s*\|\s*(ba)?sh/,
|
|
63
|
+
// Crontab manipulation
|
|
64
|
+
/crontab\s+-r/,
|
|
65
|
+
// Systemctl destructive operations
|
|
66
|
+
/systemctl\s+(disable|mask|stop)\s+(sshd|firewalld|iptables)/,
|
|
67
|
+
];
|
|
68
|
+
/** Sensitive environment variables to strip before passing to child process */
|
|
69
|
+
const FILTERED_ENV_VARS = [
|
|
70
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
71
|
+
'AWS_SESSION_TOKEN',
|
|
72
|
+
'GITHUB_TOKEN',
|
|
73
|
+
'GH_TOKEN',
|
|
74
|
+
'NPM_TOKEN',
|
|
75
|
+
'DATABASE_URL',
|
|
76
|
+
'OPENAI_API_KEY',
|
|
77
|
+
'ANTHROPIC_API_KEY',
|
|
78
|
+
'GOOGLE_API_KEY',
|
|
79
|
+
'STRIPE_SECRET_KEY',
|
|
80
|
+
'SENDGRID_API_KEY',
|
|
81
|
+
'SLACK_TOKEN',
|
|
82
|
+
'SLACK_BOT_TOKEN',
|
|
42
83
|
];
|
|
43
84
|
class ExecuteTool {
|
|
44
85
|
name = 'execute';
|
|
@@ -63,19 +104,55 @@ class ExecuteTool {
|
|
|
63
104
|
throw new Error(`Blocked: "${cmd}" matches a dangerous command pattern.`);
|
|
64
105
|
}
|
|
65
106
|
}
|
|
107
|
+
// Security: validate CWD
|
|
108
|
+
const cwd = args.cwd || process.cwd();
|
|
109
|
+
const projectRoot = process.cwd();
|
|
110
|
+
const cwdSafety = (0, security_1.isCwdSafe)(cwd, projectRoot);
|
|
111
|
+
if (!cwdSafety.safe) {
|
|
112
|
+
return `Error: ${cwdSafety.reason}`;
|
|
113
|
+
}
|
|
114
|
+
const timeout = args.timeout || 30000;
|
|
115
|
+
// ── v1.7.0: Sandbox routing ──
|
|
116
|
+
const policy = (0, policy_1.loadPolicy)(projectRoot);
|
|
117
|
+
const sandboxMode = policy.execution?.sandbox || 'auto';
|
|
118
|
+
const useSandbox = sandboxMode === 'docker' ||
|
|
119
|
+
(sandboxMode === 'auto' && (0, sandbox_1.isDockerAvailable)());
|
|
120
|
+
if (useSandbox) {
|
|
121
|
+
const result = (0, sandbox_1.sandboxExec)(cmd, projectRoot, {
|
|
122
|
+
network: policy.execution?.network !== false,
|
|
123
|
+
memoryMb: policy.execution?.max_memory_mb || 512,
|
|
124
|
+
timeoutMs: timeout,
|
|
125
|
+
});
|
|
126
|
+
if (result.sandboxed) {
|
|
127
|
+
const output = result.stdout || result.stderr || '(no output)';
|
|
128
|
+
const tag = '[sandboxed]';
|
|
129
|
+
if (result.exitCode !== 0) {
|
|
130
|
+
return `${tag} Exit code ${result.exitCode}\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`;
|
|
131
|
+
}
|
|
132
|
+
return `${tag} ${output}`;
|
|
133
|
+
}
|
|
134
|
+
// Fallthrough: sandboxExec returned sandboxed=false (Docker wasn't available after all)
|
|
135
|
+
}
|
|
136
|
+
// ── Host execution (existing path) ──
|
|
137
|
+
const safeEnv = { ...process.env };
|
|
138
|
+
for (const key of FILTERED_ENV_VARS) {
|
|
139
|
+
delete safeEnv[key];
|
|
140
|
+
}
|
|
141
|
+
const tag = useSandbox ? '[host-fallback]' : '[host]';
|
|
66
142
|
try {
|
|
67
143
|
const output = (0, child_process_1.execSync)(cmd, {
|
|
68
|
-
cwd
|
|
69
|
-
timeout
|
|
144
|
+
cwd,
|
|
145
|
+
timeout,
|
|
70
146
|
maxBuffer: 1024 * 1024,
|
|
71
147
|
encoding: 'utf-8',
|
|
72
148
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
+
env: safeEnv,
|
|
73
150
|
});
|
|
74
|
-
return output || '(no output)'
|
|
151
|
+
return `${tag} ${output || '(no output)'}`;
|
|
75
152
|
}
|
|
76
153
|
catch (err) {
|
|
77
154
|
const e = err;
|
|
78
|
-
return
|
|
155
|
+
return `${tag} Exit code ${e.status || 1}\nSTDOUT:\n${e.stdout || ''}\nSTDERR:\n${e.stderr || ''}`;
|
|
79
156
|
}
|
|
80
157
|
}
|
|
81
158
|
}
|
|
@@ -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 {
|